From 104fd5571ab083eaf328dc1c6eaa6b2f6a6dc53c Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 21 Nov 2019 13:02:44 -0800 Subject: [PATCH 001/128] [DOCS] Updates Reporting docs (#50989) * [DOCS] Updates Reporting docs * [DOCS] Adds more information on roles and privileges * [DOCS] Incorporated review comments in Reporting doc * [DOCS] removes xpack from url * [DOCS] Replaces low values in watcher reporting example --- docs/redirects.asciidoc | 12 +++ .../automating-report-generation.asciidoc | 4 +- .../reporting/images/canvas-share-button.png | Bin 0 -> 559 bytes .../images/preserve-layout-switch.png | Bin 87973 -> 173498 bytes .../user/reporting/images/preserve-layout.png | Bin 522160 -> 0 bytes docs/user/reporting/images/print-layout.png | Bin 465948 -> 0 bytes docs/user/reporting/images/share-button.png | Bin 218341 -> 136368 bytes docs/user/reporting/index.asciidoc | 77 +++++++++++------- docs/user/reporting/pdf-layout-modes.asciidoc | 31 ------- docs/user/reporting/watch-example.asciidoc | 4 +- docs/user/security/reporting.asciidoc | 9 +- 11 files changed, 70 insertions(+), 67 deletions(-) create mode 100644 docs/user/reporting/images/canvas-share-button.png delete mode 100644 docs/user/reporting/images/preserve-layout.png delete mode 100644 docs/user/reporting/images/print-layout.png delete mode 100644 docs/user/reporting/pdf-layout-modes.asciidoc diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 920c448acf6db..65de5c4f93d12 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -45,3 +45,15 @@ This page was deleted. See <> and <>. Using the `kibana_dashboard_only_user` role is deprecated. Use <> instead. + +[role="exclude",id="pdf-layout-modes"] +== PDF layout modes + +This page has moved. Please see <>. + +[role="exclude",id="xpack-reporting"] +== Reporting from Kibana + +This page has moved. Please see <>. + + diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 72e88ad4634d7..5d35f103ecee0 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -14,7 +14,7 @@ URL**, which is the URL to queue a report for generation. To get the URL for triggering PDF report generation during a given time period: -. Load the saved object in the Visualize editor, or load a Dashboard. +. Load the saved object in *Visualize* or *Dashboard*. . To specify a relative or absolute time period, use the time filter. . In the {kib} toolbar, click *Share*. . Select *PDF Reports*. @@ -22,7 +22,7 @@ To get the URL for triggering PDF report generation during a given time period: To get the URL for triggering CSV report generation during a given time period: -. Load the saved search in Discover. +. Load the saved search in *Discover*. . To specify a relative or absolute time period, use the time filter. . In the {kib} toolbar, click *Share*. . Select *CSV Reports*. diff --git a/docs/user/reporting/images/canvas-share-button.png b/docs/user/reporting/images/canvas-share-button.png new file mode 100644 index 0000000000000000000000000000000000000000..1137a263444ea1853d4007bb747c36d7d7ef09d7 GIT binary patch literal 559 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 literal 87973 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�p(ur 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
    B^!?S1TaAD_hJPY*6uY16fULGXkLcrii12Yx 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 aZ!5p2E#w=>?PHfkgvZ|ID51jIKk?tQ;@I~9 diff --git a/docs/user/reporting/images/preserve-layout.png b/docs/user/reporting/images/preserve-layout.png deleted file mode 100644 index 0282ea24c13cda3bf59159f7f6590886f3850b63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 522160 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�vd-&*gRT#&kWl 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

    BVf{@NoDbmwvtP7i)?MnX6+&Zoe}m)q;O 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 z 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

    w0AG#yuJ@z*BZ$mkb0no)2 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%0DkFvQ~QZvbn>G3{U!(opF6GqqayDwHT(qc z88@0Ep-RF zU1-CW5|~RvfztsW(Z>^Of+hOmKWkWppuSg?1dZ^nNqpQp7Y9U{#bW`-LV4%5)PB;V zyAk_mnZG6|iYVnx0>4y>JkH*rk6})mLaklM4z>d`r)6y~&9_Ora3GWlMjjH?#U@8_ zuEwdkPcU2#8{bF=a6JLQ^#TsjPfKva%?ZqN3v6NJK|IfUYu9gKL8i56nr*z{#T zh0$@^@Vbq-PEKb%5u~r2p;Nr2^oogR`r)cfJ>^l-L4h=Me#?0>w_`H)4kf?%GYxZ3 z8%Qd95UMIkwng-~K17rZq575>441dQr>d$~;oYjt_ElG4Sh2gqX#?=Q%Eh~~@Cq#^ zq@So~ew_*F*Oi18!^&b<`P-R!F|7QzVFkTg*lb3TN#tUtG94y0EOZ>*Q7zZ*ml%6V zGaGZaQ9h=5}jnT z1o;6~)om-*o$&D*O){d!9{M-GK8xV!e-Dl-^%ikJH4aF52T4~&mPL3Jz@v*$`XZG6 zv!HY_Ws!_hlTjeTzRkPf7msD(SQbl`MLu_t&;3{Nxvk4m0C71&1;nL-hGi&2oww%< z@$j#nz+W&pTV1Zq-Hm?2Nt}kK=pg6kQQs=3?}q@iwmW*4iZ}Ec;UmbS?6XEufcNzv z9F_KqJL1Hv&|E5e0;dkW!B*W29F0#W(PlTW`i*0)Gh{uIx> zhztJ|sl7-}E|Qb~bIx{=oLnR)|EK)^yDS1e?$;p<`Neay#XC&4@>iml`Nfar-P^LL ztx`+#7E6}Jl4Vg(_0P%77E6}JlI7=>EQ?8KF$q;Z{u^TOcl+i=J=LO~YEe)14~?C? zTiY(`ss4d!!9_jQ|B!H6)Ke`EoBq6E(?vbi>$%EBJ=IT+n=b097WGs=PfxWd>Rog^ zSadvCbUgUiIvy;}3M?9_EE=i&w2_L5LD%g!UI2@xDnHv)WiiAohM2_=^V1<_F{~_x zm4B%PT=ge&Bf!E+rjYWLEje;+`bN~+0YyTHY-K@g8>{ms{1O6NYpL91CAdd|YRp5E z$9Dxc_+3G$tU11QQ{dc#>o0_8v-ci(**{7zD%;{=auoQT{5u znto~R*&>y@s1R9Hi2M@{g6~qfzZMI)#jvtyp$_}#F=RLYskxn>qpDMG@9o`R6CEFS8;)SUPra!J+M{_kJ^5u2(It8^I1 zacOTpY{}cc9%@+jIpD`a&t(?Zs37GOSWka%(r<_xE_P}M5M7Tmoh0F8tVk$*!C`*7UQk64MslZSv zRgP25j>~#iDkxBeSUE%2g5NN2_8sb7{neRItUp}$wbmCJ-z|A@_SNU= zKS~UFpP#7{d#0# z($lSDS1;T9Pvz#5hi@sq^FH$w=|A=O&S(<1xv7k>{^*%JY}_66M6Ymo+VI)%31nUo z=hmav@9kWi{kQJC|HEf}=c6B_Yd!wJ(We)V4j=d=;MH@V8@{)5&bU|I8{00(FdNAi zbll0645d72>`rr4;*vAV{+&drU;a5?g?Ao{L z`7~8;bhR)$+;Tt+6Ri-C8TlYezO3v_Zb8-Q@gduh?@ZQn1@k ze{;DbxwGWx7dL!@(PJkup9Np8mORZC*4u0t;f`HtCyOr$vz$w>wbH%*G#avH6R~ZL z&F;;%5%*4)h<9$c7H}&Yk^WD436A_HT95XLCyyU}Zae0>Ml`7*Y#jc;^F-726IZ&@ zQcMG!vNVEHkOTNVz8LF_<%}1;l_tfH9+b+CZ{cig z^w`||_4?#yb9?BuE$cAP#+Q^`bN28#-IkMb%Lw;Jn%(?^YU#)U&oe#b6VoR2w7XS@ z6>j&?UMpa4y*NKS|L?Jr;0(3*R?f+eaG)=ARag$0=(1W;Z+d(`!wQ8Sui<-P0GZ5e6!*1Gwo7uvDu zkM3wa>Up2ktOMnz^=-K`a&zbCmWp9rXj&p4>u5*p?ql^n-yQRyC`%tLy0HBe36~Jt9yfBW9+rS}| z)NBpf_3dHHh?iNy=4{w?E@AB!*NoZIJN8jG?Yi1vTHzwX$-nBE6qEMraBj^STH%W| zM?CDGI94@^u4ZWPjg$HK%d(Ly(%lMs=-g~EsmY4VT}9iv#$4iMH9gThl9a3v?ROTg)xz59`)S2zWI8@t4$f1XR`ak z6k6Lyh?J{Kl4=h5%8t)x2AvvO8Fke>q{7pszl4+N{LLzV+r4jxz&B5_lz#T*Cjl7F zMvpyci+y*{CElex>+p6RKmSN}O88?4znN@f;{9|RH?DU$p?BT-GW-rS;mX;H_jl|o z)5MvIA(6GGE1$mX)7x0QSK*rUa_FnM=7&pjYc+Xk-|*prn|%Au&d>I z8=K%w75p#ONaBn~Ll&eup;#L-jH3`>GWOY%lStQ_6v7*g$4-cgAMi?JyKRZ@)Ya}j1`x;M6nUDSE zf%)vISD!AyMUZU6CvQCZdc~=@FBoQ?G8x!(TaUNLgl5`(SIo$=u8r9sA}wCNrMB z5MOb@+B^;KC2ZBrETTNYcmprdpZ*9KlV->%3oW6fz z`jq^{^s8p@@1;G{<3_`!4fH?C_T2I$+WygK;vvyn-YvPKk@$4dVtHy6?$OuST|%cd z;y$E*FR||v;zq-bV~@ttg^%I5Ol)w#dQWV{-MhULQ6uLHO0RYhoNZHUO{=;D;cWrZ z<`0}&Z+uBp__P_Gsj!{?f9$_=;!=n1bo-Mh3gzU?SLolPQ z*#gHAHj40SESi9?1)F;W2D;s$I$(`+7A{}*F%uJwueB~2;y5vas$V%I%{L!&-nzbK zxRNB^>uI+-Kquh9bMH!K7IGpSJA!ze-aN2tmMz68YOlA|I?`GvX#;vzEtv({a!q;9 z(wdNHu~!V&07A0xakg7Y+F8uPyN#{q)QJ@Hn$z7GIG^c$3}$sGGn}3in39oA-Q00vNnqW)WrTSW>?U;<^)MY z{kiNS_pvUaP*S;_Uh&hlfm1%+xRI+hX`Lr!X98bX`>murF0oX1N#7Vr!dBI z$#EMA)EYL%yI!Dkn;zZrls2qR6u%$(bc=;WkPqnm z8MeF^A@PJi?!CX6PxnpWZ<4PQ_7HrI&+k=YKcqS)*c6;ZU$u{&w>dUHg2`PXubVo0 z#I(k#w+~-c(_n}sC%c*C`ZcO3)6~V_B=2IpcXSw&QS#(^jPTU}Zw&ZLVi%@UQ#|+;xOj?h&u&Tif z1w7_2ktkTbGI!3$#ia>%RDZE&y%6pBxPBs*wxv89AbE282r>eVc~Q^AC7?I^!nr+K($8s0nB)sZ%ZL8j%1OQdXdyIZVQO!8@OK&iB0~SlNCON2&y`do;&NVY zh)KuO3bZW@gEmM?kB#+b0vY$p1Gj0ihX_inj7r+8z^GWb-Kt#q{9~Nj_8!8aa+l}? z+Gu4w|2HG~8u1%_W5M&!qCE$tf(c^Mk)530^@%*$_lkp3w1Jl*%ne1g#5T{y^(eg!p8#GB6u9CdPwsnl;jnUj{f{?aUaTeTP`Xo#O(>BWV58`eD(3ikcKc+#0#wc~6NRkD@5`ff3!pFSP=V{+Rl<)TEP4i!sO zuylECqlz$LSHLC=WUy70BhC5LJJBLco69gewGkXR$)FSVh+&U;wx-FaQ^1DUplOI@ z0o0?YPfjX-$bbx;R1irTOmP@Cj)BNGu}5_BSI(>w6$!Z2>V>>IAD@wOjOAS)ra8=P zVD|0DHk)&Dl?8)q%)BDbftC1E_mxRHcS@T* zBBaI4B#(=kZN~HGyfN&|Uw`j20p}2J$hOZiW(u~CYr_!QK? zE;=UGW*wv;c_+?y;g5WSZcHi32tg@R4G15S|Z&_Jnt7Ea~ zS>{H}AR}Chg7U_lMg>mwEf{ z&)PNf5pc>LQ&l12XQnRpy=^inA{?u1`O<|;h$j!+P!Q89^dcqs3RYhM`2G35Gpq1M zNo_<=Z}?0^@zBabwZal>v1s*V{J<;Nvi5HXO=#xp!EX5nOT+i_OP~mi zdNwC8HD215nFqCw#hpyij!)$bUlG`%H;>z;xx|EgTyjf>R;J6n_wh09+&@oDBA6p8 zaxo$MkXtk3GRNYKr8n-%_Gzq|)^tq{&K7651@u)iI~HY+Zk5!?D67obshJaO`dXg`Z>puhBsNB@{YhUNgKx8*!Fwo#)=oCZBDPqs-?d& z$acgzw=9iE!o?h8=f!3Gph1OhT^VJSXA!T{pk91nEA@xrs_OTLtrY?|N??!2%S<*k z*l`_7N-~cN2s#u+yIT;c?~sz360t#3WbT6)&IOH%_mvC`vW)}BN{bpE1k*CZ^t)z1 zci3v4RU@?0JSrvvA6i32c3p^!$z%dl`vu%rp$^~0$jrF(g*eqd}Cf$Ad!{ zjD6Z8>QGJ^Wa$KW>s{Bx`=mzcQk%n;uQ`c6!%~7RB=S|QOYv@1X!eY%4>OG3GGe=0 zvhXJQrQGTnV^5IV8Fqa{isN)nc zC{G{a9zrU?+|8_wby%!zP!6ZHA_g9P$LF*lV;WvVSgXi89e@k0-gr+}We!OWou$l!Fww91WiK+G4i3Qnbn?F`3CI!Lse~6I5omh;% zo4njcXR>oIIfdEsWxHKyiy@5!8DRscx~o1WKE`pwvMm*m0ii!T$woSA)M)_R8Jy562grs|`{Upe^|A9Z&-HW_imFNB`CcQmGS2yF> zbv|^_-E$oGou>J)zQ={mJFB|1VNQP1{Gxf*mH}CW_$PVgB@g=xu+`l;tXOr;g*<`Q zIP-$l%#D#&LnqwO`)ByXdF-H?>gu5u15A>g1nSWSa;l8RP>L*K#`+N%l$jrv^ulT< z4t?!Q{Nekban-p~1$K2&0`4RaSw@nE0Qqcrt(+oHeBy2QKpZJhbEniFxgR{PxBt&nvfB^mxtMw(`;5>Znl=C06g-z%yPi9; z_B#RPci$(pvXka9-#hI3-M{}%!@fBH?CP8M!7rkJ-xm7UFWvv|{XiDHo1=BXj2Kmu zKh=DvK5TqBU+OXBxUsU>S&P9;_Z8DOt{$ur;qLF#$&#Ko*;v$7%?C3l#oS9iLyoJd zs|#aG;S{5@s?LqoL;>)HnhzP$@*L6Sd-_KwjtkvqSfdC?qd5M->7!0*Ql8 z9wrfclqEq6pQgNv79jNNn*(e3571MG5!(0NN3E>sUjE{O&gqTsG2uN|BM`5#8vgRl z83>SogvJEw5vd$vJlXxjq-x)q!79BcE3cso1~DWaq0G2I2VK2Ey(3-9`GwhRueKM6 zT(*4yD0(E(yHaRLChu;;(m$HIvc|CF`W_xt|1^jOr0j%0;~wQBHka+S%+|2fU=CP6 z&1jbsU|C5`s(K5aRk)Si^T^X1_zWZi~C=hB)0X-v#`~uvD&_Zep~`y3A@6Flova;<8DlBBbPGmnMo`mIsU?vZieGl)2B z`P(FViFs>X8h3;0b3Vf`^Wv@o-80@WF?!IwKqTdn?`^|eJXSN5XDg99mfLj%`B|o+ z<@0GtUdlfNTkkjuoRJW5&J#9&3ozk1>%eg!jEzTfT13U9<;Apz`8qc~^;ii8#WoV; z5-7l8);@MoX?0_F7;@%BG=nfKI-ss%Ow_V0q=&I$GSltEZR5%2M*LXv_?L)Zez6yO zu)#I+QgqKjUP>55lRnn*&@^9WC5^~(G?KTsjnAunhOu?;he3b~S@P7o4Y*}_LiC55 zPi4S;x1AO*THnUnq3q0g1C#$xj?O<*Mpk~!S%uLN2O5@8@gn-YeV;w~@WsvM@dx6ROj%j$1Z6tXu zfPNkE^WEsKehrQ;gIFn)%LBL1V|Rna7fN^HsX0RHlv$M7sz%u!|Jwpmh##k$HQ?1{ zChl%AUi2>qoA&4juG!uq_Z|)7KVW@+!=C?|UgPRxF@%5dXZl7+Fmg1vjd`;M1d`#? z3rdafW?yc5N>Q0qy)%nsES3nF+lfy_Pxxyv&c5F?%{7DNT0yrd%v9V1Y_)%TW{{!-Rci@usuk| zmV%7o-t2~lTNY8F#YPST+Q5}+sQCLz{e7kWzEXc}mU^7yeEQdS0sId_8h>Z3zl-31 zF?jv`mFnS-dobHi;9}Or#UQWbPvU}{pbn9O%Um=y`A#w15?$rhZXAWm$iHx2Fq4V; zUxU@u|KEXC302o;Pmd^FSmVqqLIai^Q=eG9;VIPVyc(JBl(O=zKKjwOl!LVvJYBKg zL1T^L!){C9+OgXp4R|pmp&+!0@G*8|(%!oIh6D4$_w zEN(5soJtGTlzC;76^1&!R$giP%wQ+HyGfIj|fu%3l@6m`~Sjdzb<>f>Q?t z^AzV{?*wwg@MKfE*M|`wNSd#xl;YYe3ovYgIx0H)5d5@tSC$ktmpy(Jzqt3eC?A%@ zAJ^BWJV}jZHZDXvCYC79xGr0wyg%oK``DeS5oW z@*dc~&`sW&_47`6yDpS~^2rNJTmty>O^71EuKYh7%m8jGD@~W}$)J40d3CdOvjhLo z(>uL>%$s}216!xeWaZmmpvFh>N2A-38uv$^G4^WFnUG}Di02eXhT;UyJoUYhF?{i? zyn}TyaQku^khd|$#Ii7m_2YQIH_3##vYgt|aAf7mv7cY6x(h%>}zhy_us6 zqbZ|#88&d`Pi?LwyB!H{_e@Ewh!FFO9DqItmwv5%eJ4l+3bjsruS z#0g=>&1Jq2&INmK_qf0tCE`^Ue>4359fl8!24de(g%;K%nREwdnApzWt(b`Ty2gV9FYSC`K#$h7YcN#?aWFHP5CE4Lkil&lPc)vP4t_C$2G< zQgks#n-G5@D#@EF({drb@@h0_D1M|ab3sq+tlluHbIqJjo@2on+mS#zlqIdP23C9| z_)}I~h!yE9pt)3|dPAvK@r61c({qf)#iZHO0kWmpYak;Y^vHp>-ab=NuX`3n>l0|F4j6D`k^cbpW3swoG3P@;Pad2>Wp&rh7D6C!e zL8%s`M7eb`22_R0YELtPH|+KP$y;GjoIG+C?6Bk;i5^J%ulqfIRRTf;00jB9vaBd`xzL$*4l} z=q68MnGN4syjih5>PM^Nd`KU9b=bZFcJs~N32{asB1->dOpp!D49J7|sVvQ^?m zXnsTSzP#wC&1dXCgAtiRp5q`y)h55Ej8jzf8D zEkMEZcwxu>*5t5@>wQdK-X-tonw%CS!%aHkL@lqCB>Tu}dyY?&&30TYE>KEBvm|NK zQYG^R%id>ZuMF`t@jm6iWLi8#=}S?0XGvS}-DY0Nr!iVN`KKk~Bt*D^#>W?k6yi?B z){Q_NmetEai(B(R&Vfe8M5V#+xpZDw}7RtAt60@-0 zcs!`R=2!KsJGtRbklDs3X9pUkfIqo-E*u(fF!pSV(L6`wjQu~}ftF{dSP%;G-e=E- z`|-Nl&Y+Uk8#Ae%HK#TRJ-};jU|GgE+fl!{LCZv#>nY1Sj}JS(cr0}K-q;(_C_G~O z$m)xYnN^w0H@!`-O!6*w$>sM_ zldG!*ZAMuy(${Iu+1u7aE*lT#3vC&1YBV4_`d1#1QFsIZ33KVook7$ElK5?M{GZ>J zM)XqD;QEYL-(6PZtKe%%O;~?3wc!VPmVwD2R@HZl5f=fIT9r$~9zvDpO>`tJlFhrv z^QYxVTRL;C+aw3fe*LMd3sx2L^U9B^FfJBFdm#k^Z|#GRAxAA?Ii`_Gl=|3%{Xq1W z`hHAWgf($b{*5&fZ@~*3HTUR=%x@K=vwgfkKL-OMZ4|V)#LO8Gw;SHpjZLsi-1(a{ z@5|j1D@gD)BO(@-Nt5NUrUy9&TP#SMwDL&UZ)X&Dcs=)IdWF1HDCk}2 z{kd)py7SHTpW%A*pLuO~1OTxb(CfEmY@&pMG=0(+Mv=rZFd(*z|9i0>DCGw>HAV(J z`4c$Ws}&35MT0Fl&6+uR3whB4i+sA=6W-srCu}?96UD$RSNqH|zj8#h`sY=`4bSdO z2XHw+e)#r=3vvTHG;wv5DZcjh$XX-o18`K?qKXBfQZgk#1Id%|lOh!#wZnhPnXZKb z0O8>hpu~y2wjkq)QOJ2-AY8=*?z-f)ql(B8w}R`543`1XDBmfIw`#VsjNC-}=(Gzo z4GMI#tihS><$30~oDh6k#G>m?yH zc`p*VLieGC$>?uS1Q2TH%PxmR*<1)hc7qVEm2-opPK$;?M*}?Yu~jP_Z9dc?60tLI zjlg+7V3HNIC0S0dlW~ks7mUlJSYO2=H*AsiQ}_A;Nkf_Nq`4yUm%vwnO#K4^=_vZQ zzT*7j+-@@uVy3ht*=F1bT0QDp;aO6?iah%!;-aFcMo#5|cKN?+2GB&s;0t zd}}~sJvNVX5L|nRUTF>V={i_XOUlL_uq%8)F%7_a%b&5;xYNMfz`hPbbOJrIQ)+op zK$vwGt|nyV)A#JP{YTk@`B1>l!#$Y@zsOhBN>g|z?*?Yon^!L7A+W}(EFTsM7LlVU zu=HuC{(mKNYj6J&LebWfC5z3s>zUmn>xY2DoGn?{}U9|5-be)YsI04P-Tv(lDTu62H6hogV~y`%>8l@is75Je;W zf zdYC`RKau?9CUt`CMf>SJfM?v(7o4idN(BV6-Dz`Md2-zr5Ku0##v(`^k@`kFjdu!Q z$mtFwxx5}r0IL?ddif)v?pF1M0uly2jc}**9;P@1z&^#5 z7?|i}%*yl5l-MrP@4g?zuo9TdC38L>@3@Fb|6q;_43{fh9hSE%)YsDSA)49|CEGXN z-2c*1q{HlWHz#yVM*uBOZ>hIl7s9J48ngJut-%Yyu?13Ep)SD9`5Dd@1K?>;b%92) zN6eJD^`3o4&P0cD>YJn|{r&p|b9_Wd8Ii`^8e|_#D;@`a#@4btu)jBvE@Xa_en`>B zEV|a-bYu7JVMc608#!Dx8sy>`o(V`ILWSQlWfLT%nlHqhS@bsDwf|AVwB>=izx4zqY{|7V^aNu zX&K2A5^)i?`+tg_XtlFDDmJ^C0Tz?S1g^o$y{@nAAv~dP$y2n>-VE~YCuKU*6ZsFx zci3jb6seD88hCRXe%wzoDj zok`f;Hjokz?g)wRx=p;>;$9&9j#S0mGqsW>Lr|lS^A=vMjQ0GjC- z_yp2aziZsulH}?gApeXCL}5Oo?ibs~SDA1s6A-jCWB*?^S2#BSk3XJuq4VZaJjr+m zcg*Q1KbYzLI2nqPO7e)BiiEL)q$^In#DS7EyzP%wZzYdDV zChB)(9bO58E8E)?$5+Qa@agwckIOKhHD5Vc-SNd&$$i{nm8hCT?6^U7!qX&M0~6^XWT`iN)sR9%pc@bcX$>ks{y+@5KW;y^EM}5d@4a>ZZab zaj1wKlBz$FDHLmt4KULJoilo$#UGbz9P@O0$``yYNHN%cXx03EHLB&VmB(ot&EuFo zWo7!I?3czLBKYL@ftu=HI2n5(jSyUFJXx0~vozt_7wWLmhk70_)Fkse&gmBOY0qAT zqk(sN>T8ZEwJX-NG$1BVMg@C&`2wtuIPMQ)MEhD;S1(?k;@VEQz6LlDyIfyG*kA5z zCssoubfGVD#N4tFx9U|Cf#PwO=$x@76^ujbHyWq>z~?uAJk_&2 zQ*I!j@RRIn@u%V}VyYrkwxD+X#z(hjPUeL7MNLsw zS3+qg3Q{=LNw`>2ico?fZ>~IlTQQA&EO>%_M=Dd7-ry7s^^1G#5bW3vw;9{$T&E1O zvQ!X)4eSmUF|9;H?aJhGp6!htgXJySS1ZOuy<*vp6$7mlDts?6O5Gun4G7Z1FI|yK zTyvLfQ$&x1YCsyf5orjsGuwQxw&TY5UD#US{+h7;S~=U=+LTdC_mJzerSY|2I_1(e zeNO{$lHzR}Fsqt;=FZ17xCCO2Z~!_3Zo+p!DlL~7YKoDYCbnYh3D@8rakqqLPgpJ4 zUKN_{P@=$-QT28OBA-%p8P~Q}JeWK$Nt0V?hcpO?z~lGEN@cZF#D@%%>yy>ZBbt(!#gFmhJSG2zBm@uXLLqkFmtqrkf@=D88R)eLTUipQmWN=sD4~GK3cfCD0OA}? z<~+8ld?K8%D#7AQg9h00uY?;821}2K>Gp$f%8^p9rl>ItUyIgM3stdjfHEHe57Zd_ zZIcT6mo%wzHRg0oHiR+1$(C9=-)!Hh4+-0Hz`uE;zho+n!Y(mXHSQxE^YG+2+iFx> zxx$SMpV17>24cP+X5|*3y;QmT;KhM(2_s?RfN_!rrSCAA}KiN zw616GTlLoNf`LO?nX#j@1EonIDC){cO<-mKXKtioZbh8Tgk3N2$(M*O0HM6nkq*DE zKhDD$@J2UPW!NhS#$J%>jqWI(1ma5<87U~=jlP>#l>G3VW zs=~~%jqx0oziocv>cV*~Zlg|bcE-hl-6VDg6a@G3H-zExz&U&VB%*Wvs)#nJm;tJ^lmxw2EwIa2EP zuFWXwo5P{&H}bQ0ZtQuC9JhhRy|MVtmm@%5lW`H0&hnc2aEfve-9CX-&KBC?3@zFP zYVPu1DMQ&^K;AZsV13xaCVEE#oB%G6?vN)O!1mvG|8xNVv$l(>Y!M%`j4*9T}6UT39b&oKblaiYISV-^trk|}BBO`><0H@p+V@?tabnmqv369>PL z2aeoI1}#$HyJG-uNidJ_1suOz2XLXB@dDHTG%tOZCg7K*_o^7ceYr|)uBf=Jt?>Za zD_6J41iuKgbjCw?je+YiGYzy|TTc15yH~GStL9EvKQ+&A!sYhn@G@FDVMgdj(rQ;) z%59hWWg93+>kQ;dD_}z4vZS9uzux>YXIrI>!9PXs0K~nJ^JNW~FYtB8I1wCzfp7Dg zSm09tOu$Wi3X_Xz;OE3nz+s-7bCGL9K>?&$hu_i@f*<$NZecI|(X6`kmzY(#pe~F* z4rZ-~GW@8gXPxns9tQoYVRDT!jbpp)AkKg~-U?WQeG~2hz;=0J2iX3(4fR*-lU@5% zNhUgRm3$Bdn24P>?Vq50FSa39^6JJ^U$j>tfuHCR4sCzI8{TKly<^XqG$gI1^QF+# z{Us(~^iE{#t;suB^OT~=HTSx(Urn^-G-v|8dalpjB2mU42}ua{)%`D0NdXV-DLU#i zyx@=>-8*t}Or_};ogM|`rWa0HWsp!DvS9&H&&u>-SiVUN)Cj>v0sX<08;Y#`K<;>P z%Qq01PN;ZaTSWU>Fnm^a3jTY3{f4g|AlS{IoAoFTd(1_F=@=m^N?OI4p3%aF>y(a8D)WZTEzHiViW0SEcQkBV589f~tn zS8=sure0~?2imG1@$a(V9_}3RH{-(ATnB<;&GRn1#^>>RfWv)nHIQ?QgOn~-YrdR> zY>-D6Qm#AyBC6`rJ9rx06pV=)kgG^0#K+0y2H$;66cU)GAhN$5A@8+ogiEke-Yu7) zI+>F__7Xtps+#_7*R#%8(0$l;sUb;)AJc^ZS!KG2<*_`Ppx^Dly=T($K-UJ-uZg8!NlcP;Lu zM7pqD>$IXnDfFggc2@5pTih+?D`<~GSCaV8^h=YV? z;@e0{i)PLQ*dH(eI&=LN=0|@W(=l}WY0N8Er z-LjDcHZgy;Kn}S>`}4(v>UIO>G5@*-2se-{cPs)xN|li{n}>LwetnD)nQbOMr&o>z z!VZ}E*ev|b2vX2r)@T6M62Z7qpuaNp1paIhQV3MzV>*j3ZrXCAbuW)*5TU9U1?(7`j5IGvDT%$eG(8AX>wMlJU=}V=mgcEl!Z=2ZLqa$}XMK)8xDX z2yA(*3{Vg{dwVV3nOO^NWQM)s_4@@hUp%tHsx9TONOoGGVhC;xb zQv6>c!iB*(es!k02>WkPH!CHglaa_vmHF|-rx&j|*4S*Z{kgWs& zsJ~Vzh~9yp9bop?$&`+lmw`bDCu4RG06?*FRYy;2of#Ql8_a*aDsCnJg*#xwK5wI$ zNKH1k0>thgzoHVQNGKPUeB=>*icMUUlW!N>xW5GCMsJ|c>cUsJr8e=+hcHJp0g-0ABF|$ zTmH>+uH7d^l6i>2xtbDQGyMc43=mr{35VcM+cqMBMU<5HH|RxPKpG0L?e6l4A&z$_3n-8>k<6+_ z3jK6lm!o1=UOTh8E@?{EWl;r#t0DtW7`l3%v{UZcsga`nRSMYLc zCoG-qm?xaGCp3xD^PiDNm~F>OyG*00VhtK3CHbRs~w-{BW=^wTGK- zmcP_h6qO_%cJ@n(?Wvk}s%TribJW-1-o*xHgTDF`v{~U}^boqi7`fO4i0^&c z5{D@~su{MEQUF8#8v*}o5Gx=??zxz>w_x1H_VYr|BWpYvwdY|FM$*-uR^u8JP_&1#UyB6(VgmZVOpR2dTwCY|-3GYZ{ ztjzLP=J%|H;UW^si{_HIE+xQP&V}rfQGH4OYhmas&Sym^PA(-WP0~IqkDxBh10%(` zcKdD=M7;(J)~ae$o#;+hFZiY0ES*n@tiBIRIi1(|y3POS4`&7P z^R5^*Q9tbiQOZFfyGZ|-Ud8za&97r5$;F(0dfNJ&U#_!(7Nc*Qy}eE8C&8R&>#0P8 zEMM{H}TxbL4688`>dj;xrP?w3mpkrh+#8a|swzg~G(9b-nhl*buMc2OCGs|1$9i5?SlT{4 z2D)sx$A^Kf_45jXt|`{6j4w@PL}zKuVK{R`)62610dQP4;(b)0EtaRD zZ_w6%Vq)6C7z&1RI@MDhIZ|^iz8Yu!vL_GwV=}bHc-t$7mwK0yIojTJ#7Sxy$>73V zI^vH16eV!}pYvlM%Le1UyVXNU)za-RBo?(psH9I2y+CF6PZPvUtyMjvkV+#Z*yPC> zYZ&KQfdWp3+Bz2Sd8~eQKP6BN)U)*Ky8zHa^Z;>uYZsXQWTLLBJAYBUXO33`J&JRY z8t&4}(qR3hJ|4y^+JB9)l9uROZ|t7sD;aJ4xq@W)gjyVf8y@^=`cb?~fPYq-7xGN# zQ5F~mnJceD`azI#ZeXJc}`h<4P31}lxKfl)AfNHeuV!UTu2@>QWf1hpA0F9yt~ zYpq53p}t>U?1Vri4}6F&6LaPs1lFtCxZFn|(6z3oW~tM_e6A-t5zW^%KdlEs(jJL{ z{g=Jap9tf_Wr&SddRm!w{AB4}tz@-v=EO&BALT*caQ2K>wV*ig1yqYAFbEnTeg>aR z4uoi#twUmJJH6N>>6aQ_aEj;F(ekW|IK=ZcVd+`DN0JvPhvDKjRj<)D z#o+5lSNmU_KY*9QdJWs`j*A#{Uae6`Q@sPFJWU1mX}~VFzHGl9L&JJrZ1bT~_Ho{o zw{4AW3$JY$d$rpS+BK0R)>Rf~s#lj9B>M)y=iH?6!moF4&Jv7;X} zf9QyNYpDJ9e38pqv{=>m(zlM@p|3pT%OZOVSg)1)2?Qcwbm~}zL&=Muh;f9M%g&9K2hDk{I+p7Es^aL)J4l?W6$=F_v;8HP)OI^ zGRSk?Mv=P31@Ks&npQ+WgC0oc zmh4Yb6b1t&!Y_UNx%M4;XPhU}XB8r%7bp@X&<-y8$?F)sQvoRN%@gZy7f3WYDz7|A7pekI^yN9fz2an$<^l7wJhz zh5>=Zz2k{M(ukryROGb3FrNJ`DwGlR8Mxqc43@W%BJiKalLt=Ze?w41YP)oEE8tK3 zG+u@0u%^w0Ok?fFG?kaWK}#2L-jfUEk|C|;@(9qF9d8pZ3>g&#Bsc{-1$HdY9q-(0 z3=NP0c9>*=fK5I!a#!;R(VGR9?u}j144Ui3g*nf+WvkDf^x}bw$5w8B1Z=)u1-Rn@chmM;r)8=}vwEt} z=CcdMwJ$NW5Z-E%Sh$fG_}0-k-w;wZUsjiXGS?2c|I>UL@FSc52o;<>2o{MVuXTAXN&vllJy(3L6 zkaR>;FYZ)3hJd*_5RkaDdLQ`Wmm};KdrL#*MWEf3y&@E4BEH zjhC{!AgM$|L2xn#NNy0c5dd;XQUJXp`@y$O^}bE_hMF8W>{X3yASZlMDK)g-9p}yr>U_=p zhP`6Vg4Sp7QR`y{xS*$C=ZlPusW2@dlOVMBB^SUZAs=f6qO=dP#3j_bE?d`Ff05$- zIBo%4UPvOO8do@3(z-jn-)(STbUz-uK!q&&ZVVP`A0!Q@8hZ}jkSC380Z#?Zb3kAh zE<+l|%0C0?YPp=AruOA!HoVwEG~aFTT0oZiVSs&#;1^<*DyRz`rcLRmMkIJdZVok{m(a zL3>tV_C{lyT!GbZbbn82NJ}nYh2@+tGVaNe1TMhY%_e@^-EpU5-XS(V>^5a+@G5K$ zo(ru3=dl*OhLm8O4F=^aeUP80NT5B0M z-mnYF2recctcgxQYSP6EwN2Buevs$x~N!#T8~kmj{2{p0YQRm021QZT&nk^S7WpTxE(^*Xvan5 zaM%AI4f=1woIO&@zLy@LZN9p}{e}Z-F_LAsT_rg6Nm`pn5baOMtDg@B0tk&BbVS(> z--`SRBO#7CF$vc6$cijOmv`$4{M1mWoJaMSL!z`D+FX+PH2<#Y|J`c385b3}9o6=# z>(-nLRSaBHnoQfD$I6gdd#Vyj+C1Y}sl+~{@^t+?^T~S(c<4lI5+`W z#e%sM)R@wxEPUU!*jokS`rG0T3Gmda_SVBgLe~ML0o1xS|M%=yj5lm>yZL@0|d+2*QOgK2_)6}Ar;`-kIUsD(+cJ?z4M|cR!(tauy}RB z8qweUhMS|wGs3gg$7|O5GNjOSprCT$%o$uW!K*R~T9Q=vKYZow`H$_3Ji^!VsN!WyhRp;qyfr}P(!xt= zfb!2>W@v#b3rJ1iY9QLNny8h2^G3j17KZ7U%M>>6d9{-ZNpNkQ_2$Xt zkFtu!k^x@`({RN=1Zgt#pPl=SHRgvWHdN*@-%KYA+WlDhkDykZ2EZkyxL8@Us;ALg zg@1%+P0r6of&*vz_pEt!t;*NASDFsHu95sxWq=`hb&Y8f+<$xc zX7+oGf_aZoCN@Q(cp>8e3+=fYo!A={@hLg7l0)0x_Adq2|wdU`ZL`A})8 ztVMaIQ*X9+(p5%sy(t|f%2>Cgv)#;8*BPh~t#bWG$Eb$hA|7_ncj<7uVT>_qM9*x{ zQONKZuaz%%0H zoW&r9o(FK}qcAbbp>`Fj<6GvMw-~S0D1M#LXf28Hb4US!<6g9e_eY@>s6 znemNJKJQ3ZOa9t1>RQxN)U5Fhb^|7cJzEUc)FE|~T0}Fs%2Egal6S?p#UdE_q}R>8 z3o3_&eqc9w8S{FscEpZQ5irgqSb^BV0;u%Z-(#~bAaY*BhK6|IkgKe;1ye}G&b(0w z@sp{v$5Wxt=D`RZn@dozp^)u}-SY8^LmG2q=Y!0YCR>72|0X$`u{K`iYqR(^_&|C+ zr_LB7bnzGZhExT>DAgi0OC349#BBj^+}tuBstJX2JMu<41hD%;FB-O(BGOKFkJ~1? zch|B3jKZwwlh+|e|G9NY;pW(2furpBjGnZOMV8Lc_}u--iB#q&WtZz1H<)AAA~-9n zsG_}L32Tf}eTd@DtXs656Xo5Q*~oaAzeiSdy>f(F9Ow7+NSMAju~I>$yC=a#zkFya zizRqTtR8$D`JBIfo%WUIqMs;JxSPQ^eeh6fp3m)_PXIGCS1#gKS@e{-+K8gY?-ayPB-70IxY=kK*Tvi>?i-#+W;nETGR5|Qb{ciP0`08Gl-#w4;vZV1XJ z+pfL{OGO7F%EsC<{Pixu1oItwqx4)ZrjxYi@O=Q?@7Vbw?qX~UjFA~eiZy8p? z-Jd%kRYym`0U6LZx^qep3^tQ{oC;7B1OCgvrl?0@DL5q1n`cx=WcbpXfW$+n!O}B6 zs|>*#nH`L$ZXryOT#*t5Sct4z|IL;cFt=8j=;`?LG33GSj`G*8I>ftEB-x}*n(Hi$ zW-RF~q~#M0iK~z2mtY@^_$-C_`itF22t&e5IkKD!aN11#Wll3j&J%TU?p<)v4~M|o zBGcg4uyWJIRGpk=;G2L|PjaAM1x%Z& zeG(|K-R?8;k0do}u~{d~5sIRr#v?a%jS~vO!77m@6Q}Vl#HmtRZsF0TSHff}ne#YW zT*R| zntELN(p{NdY6po7Er6qfbc5w6;xPaGFyXcD8{74@cNn}k?d*R(Xv`a9^RhcW-!jtY z)oHVWwn&VK!~7NnX(P!RIm02o>06sKpWQm`nk`B}N~R}aaLI)sPgan->`cfrFehC! zUw`+cX%w%9=TD{!kVT)o2%ekEn8P)UP*^4>thh!mdc#ZrODrdG9A`pr&#NO&C{f5% z;VecOnR>T2a#kyGv_qe~=K9kh0sPeWZh- z{e9tgCn^NHwlbZ(-g*!BnZ)T1gu6`ud=oF`Wz;blxIo! zL-8Se*j=_o$e_loau46O#jtJrl$b|STZ>M#|$2#gqtzFfFS_K$So}= z?)-4Tf*G$S#8@)a6!yvKY*MS;XE1qF>>JlpT{NQXvdwgLKpbXU7p+T8HfAq-I(dOO z(KiVTnT!~k&e)reHy6F0E>bS9SBwtME zSjL#oah^i*ONj?IjZ&Efmk6w^WE;?UXdP{pB1f*Ih?`*F`0@r2K|#ocaeAYRHSMQ$ z#mTFejUHo+87NqTUDFSIVzhN=6>aaU(axAOUy1D`;}Cf8W>+%O{HkwhPb_3XrFbf(5xq8 za#Xk}c6Ndi3QA~&AYHhI=!N96@oFo?)b+GP)~~RpwE~0Edm>@Ez#2xVH4{80J|2t? z5oHxWVc$Yw8JLVVQB)YjLXnDFAl|;z(uWn$Axu`br=`Jg{Z|yd`%gIZ?&Z)QbS#s( z86)&24BlRyqnPUEc>AVpmd8Bv`YtuU}VbWkHJNFCG?XIeIR(pQD%{A3ES=00OaG&r|Z#+JVQ5HRHwJCv4m)Lzl%{`|!=ZVdZPzZIw0{U(m;}9DfrLk^x#6HF|WY z*q1K+6yX}UV-u)W}=+9WZD){-o1MB(@sk;yVCR??hwL2cKKNfEwR@9|;S=w;z zA4x%Lh3WW`(RP07g^8L;3AYyw?-iCFjb4o$yi>sWMvtBwu1}rGS);0wkM1HDe1>K+ z=Isf2qn%w|d1^rxZD(`Ay@|B+z)OO0xM)1v&aA`^LZW5AF&YJ^*4Z|w{DC!>_>7a@ z_w-B^HSBv_7sd_bPpN1Jk~#OV2kUQr-eBxg1C6VH+l5%q+Z3A@aDQF|_fCEJT0#9S z$wFn+SW22e+m0x2xojo&U++Q>PLBg94Q7mng6vXCfJ&yooeUPm*4By5lP1WZY2%|F z02^V0AN}PhLyR*(^#}&(ml~<8q!<6K&%@~iu>T1d^6r4CFL6VojUWZ=sh0{L71(^3 zy6$v9|7MJDirt*$)j;9kHmbGO1Po=YciYV%w{5N7+p)~XJgBEa81jM(!Skz#7C;h5 z;iB3I-ao5QF3e62ZrG=sw6C)g%yaPl({8J*f|UnQi^bc=G+xG3P9a850+Mm{`{q&v zW~U$p4YiiydrG!?gT}7z-@v7xzR7ueI&WaYi`JC&V-pNp(H%|sZ==l~o+NVy@#O6^ z1Bgu2NgrJydk^^^rET#WkQ`D!;^N52_T%7OE?ee5iujCw&>H(5AWwuKsl~#15|m(j zEC0F#%bK^xF&r!!-IA_EH0@Elt1o&G?7y_*Q}&^nvJ4fzrAwVE${Ucg%TW8f^!>(3 z#tyZ;L6pHEm6`RpnKsaCD-$3KFOtZaM@_rG}e;_vLJ;k~faL-#3mdKrhP+3C_2`10ku8 zZPCy6hUuTv3DGG(#$BH+^ER0LOvO#@sLV<8gmSWjd zfsJu#DFr*y*pk&Hd#y(7y&j)Nf=~5NcI{=IF*omY=G&>Dk73nn2u#qbLGYzoFLP<; zHc;DE<%Ok7fQ|R6;pcg4dk&83L+qfQ_3HX$6I9Z0jU-_^tyolBd=gS4DMAG^Xs?J? z7~Z+JW8xl%Qd(k3@HvGqT=FP%x}&s^yN^QXt87_+mW65LTy+icn2F}lblX}Lx#_3* z%Tjq!{F!c?uvm$3BB-C99Eq9*JqoW98_%n04mCNX;bkDwh94}hiWD!H!4b)j-hE3X>m%F`RT9V_0pKw0rqAu7z z%FOc?ovd$M$KyDjXGt^fRfS_i(yaogAPdIH^$!P3`rwRb#~#grv8h3} z)(Frr+1jz@-5X<{)J7I?9#ENpDWU+FP~FbK{ScUkDLx6Wj`lR|GvSo?17Lu;63?iG z^fpWfufgI~auPc1BMB{-SLdL^{xxl0U7GCMl~(4+evLu=H>8De(6$OACiV8yobZf%1ykV=g)3Y`L^42nLaafVN<114@Mx?o<=3*9&?+(&XKl z!>+OYwCe;?oF4t$z48Qi4TN9n7P}8Xm%K-Zv75r@eM(e)POqo$h=a}idgknJI=w(% z*->zi>_2pGIi{^r&|g{W;5EtFy_(lkbmgi$Z1ou9p(rB(Vgm~^@V|&Q?f!V+ZP5(_Jt7sLI{5$gyk-gFQ?;|)A7sc2u{a?tE|AiV+P%G zI)(uZ&@q$=POf6|mW_rfD5Wa92BU)}n=G?{S>S`}(@~LG9cminyO&M&>^rR9zSezS z&NcT)C4R{hM<4O|+){BwEr3nWB}!vFWfvT7=9R6DHKTm8v8FP&+rX!Knes)>v97@K zq3j``3jG-_uIDcR_>b`wq5qJe-!3~7$l~|6BJ}AhbYb6#?5+e%O|f?uWe-0wk(lHt zSB63@+c}Soh6BLALDJ%M{7b>9g22uv>rQ8tS=LhOlGn7{*zwBh&C=?k*^q+J?wPlNukdF}982?O2LzIa$|m4e%8+Y7KHwd5yW0N(tyzK<-Ba;pKjD5h2ny5ycMHfC0 z-rDxt!Z^=gCr!d15O)Hc`W(Z3y!NSE=v|J+K4rZ)Nh9!!a@$u~g&3@~qMrRJ_LtF1 zT1kQSq5VGy$XuP{ppWk)*LC$sFRK7DXs$g9=^+=V?U<-`4n2>$rhv>{Njtk@J&2e;ddRvvr z5%Y)F$=Z&E-&&@vj&X~r6+Nj3wxO()6jw-&a1U4P3iYu|5pugT(68Y}8(V-ewEsI` zcm~=TNMc;x=NgL*d3LfvupIm+n{}kWM7nT~)UjRglc;a=;f_SWaA=?wLz@1%-Yz@g z<%H9u3G6e9<}(di{$fo)W>uY1XkG3iJ9pP>@4%jTmrYwy@L(H6@S|1>qRWY8-01Zt z3Dd`VrH^j0Zi#@L>%XPw;!x;a>7u%Udi%P``9rUO*y(Jl>5)!{W$CR8d&WAnlDeHG zOX9Nop1*O}__XY6Tzj+!Y_%&J5|$2v$N!o66#>-SPB@_90@uVa#csBq-!bc%_X@qo z35`uJDtgJQA(7T(W;Fg%2;_j|eaFZ$aWbFyXCWURL-u?p%f&D_-t?@@S`5C7ewkD8EMZ#-nU@qVSm%`# zIff^X=b+F@yn8@^_xgS`dpd8*=rf0}?3^ZY^7^ZuWBTW6=j~xw-qa0=9Rp%cI`|Uc z7n;7rRqQJwZ7VCJwRt2njOVn@U~T_S4AVpg9+y7THZeb?_e;iD@i^l%GI-+A-;rM8 zL(iN1vQ&z}g2xLuVY&a(-R!hKF~WXf+%N%klI4}*RN%d~$4u{a5R5~6jIBAzN1F$g z2;zX34EXZfi{M@o#ZMiqpo98;M8FQ~RbRZz>01{o-pOBL@JS{%!Gmo&e3hoptq9M6 zJGf|grKk5hyLdGuyGV%3_{JouQ#qxhNVhz|jgW$}8NSH?9!2cSPsbLkU_8bK{zOih z1CUdL_l9a-sMP{Ve;}2R{vD~dAvIE$(OBf_&k)#ME;3410%9TH!*%e@Kcrqq)V`|a zmnCijl5+FRy*`a=fwg0)^4~D({^cPp+ZY%=+~yWbE^oWhIvw%e zo{ea;T~ja+|whY^{mVQf@@I;rlNoL_N5Hy3G(Z<<@>RCs20- zPlwmw1=+F_%P8ADfUs(pYs6HgJ7i@n(7CSfT!zIKpTOGY16O=_DN(NOXNE$%Z3zrA zE|#6lC-l8n8rxtXwaW$YVK^^0sa)J<2p)I$8ttIzyqJwH%`w=%CjROgL0AZ+8cLpo zaso~@0O8BOsN|u|wg3inFr_~sZC|CKVYkmh(k?y7jPn|3fWSyM*d!^Iz@mYIid{0< z77Z*gY@qD?t0VcZ#yF~Kr~wR19R*2dRn@P5tgX*`5YrNqc?dFB^39D{{JWwI9*`vY z9Py$E-|KhX;1{lFbLDcOpqT4s4F2!t4^Qp~wo{9;w16}86+M^IjL}Me$B7vYeYYvF zWdt@2^hAkIHcHP$>I<4$#H-SQI#A>@a1%N4>zFYH1U^I?d-jp9gb}NnOqVjn+if^6 zYHxbrR?j5CKFL~e{LEiIH*F6iliGkG*S+(1la}hp@c6wKjSh($j4?%Gl-wby3i17E z;wChBgpPKdTF|L^I#~&P2MUPK?7%O)!89CX@XJ<`Jmil!`wp}^`+fw~i5lGJSTOQU zAQUCAhXT9~ZZ+n>A7%7?jNJIK;=S<$qWi_!i#4f{6Z#hj>jcX@AZgch4iG&lW&%l` z$mirlRq-DmmS*>w|NN!-WjX#oT#gSHYZiKU-x_1lh}Bx+36naLwLRjT%frG;(@(<` zS|atR#_*XpDfWRrt+UXFn{i>dZ-*+p=pLG3fZiNC61DxSBhT}y!*R~9GYSb6 z?cI_pHy?7yfK)*sRLm+fCyYGMO5?c9oMQL3kKHQlyQEmj-!k?-_T@HNGJux{CZn~sLwRZS0e58q^iW?jk+r;>kfa@H7I7}tH= zDFoU1#G5c>HUc;6&ggcKPKZNJ%{+1JxkWELqSjNdP|mR2%DB~QA70+TxMb)Tym_&v zSS2sQ#UxGFF9k7it%^2IrrbhI6clnP19cqf(V3U_WN+Ml;kkHo*IfKd5)03rYzi#H z*a%3)oM!zX-Ok^17|Xby7dnWRdf6t7bv3={{xih7;BqZu^NBbDoW- znys|DbGB*)y%eMdUBNR1tG9Yq1Z5`d0?JPx;_jtvi+86XNE-3DV5=_b%iK|s+Z^78 zo*b(y0;2H<&_Eut4qbq7$%ETWm3sp>Krm<_(-Djn#VN`4I>Q^iom!SLOt?RStuHCE zEEpko-nMz@Gcx3EdzH^L|7{*??M+u%Atb0jcJzt9hizr;#R4M`^sR!UdlF;v#iEG_ zfX+nppX_Id#)ye$8o8*;*d&rCvv=hvFx!ZzmIgEf{HD)lUHM}J2VQaTQtS)MVGy?y zQ)(DVYHg$a=_m(g+(i?W>wp%hw2Tpe;iKIZt zqkjdKsAsP5FbLfF*Yup+FpqCQxCkUyzq=Pi=j5tUwP1)gj})X{&Lu%jbkaWYV_%|O zqfaB`-BNPzHu`v|C;#_2)Im4{F|2=LApOKlsQ(!iMNKo?^9dYXK_ANm z;Oa2?q<_Y>qb}#mMlAA`BC~i2@f`+y@c1E1^Jlia%reTnJpd34OR^srTpAu2Zgw;h zQU_0;?)9}%gyIr{m^JWUXpq4idx^tT(N)7e=>#4~hJk~i_#1Tajbaot7-z@aIE~^V zAPv4=?8v_SpSJnj;5nOwLAft_&k;nPD-`zP-s$e|b9oMIRAY|y?_2B4z|!mS(T4Qh z0=NyfJ=d5nY(B9y#P?+l8s7mh-KPo$Ic4(enNKJaq?N!ZT!{m|1mtYss{{+tU6yNm zI$b^U=_r+8C`AZb+W@x3QOqElfQnkK@*O*YlMn^gf3ibfj0qT^xd+bf?@4JYRDMv9 znUq1;RiH1A?a5Q!UP7w^=CLViZVtD=P8X00++<~tjKX(4Y|hRR8LPp&@pT}BC5i$< zJjezA;vos<;#rAmvrWu2;=~B1MQQeRVk#w=X_n?#s#Qmw%$k<1O6<8kz*wi;Bs!!n zlFDA(0p2+F?m?5pNSmfiRnKcMK}FYW3JTCF+^rvt3EuCF2~N@?4Xu>n1fD9XSYR-5 zc%3fvj{H5EOb0=R*!cY)js6aD1Ym=Z^_F&=UHWDM$p3c>f_?5?!ncTkgdZxj7uo}Q zWmD{F8=&g%8$J$;_NWdYMq@lFV?%$AJo5fsT@=qs3>mI>O(M^}&26!f3Q7Bs!uh0q zvKbqx=T)gCE?18Lw#p(`loS;*OsqaNi)^S;zPS8ByiFBe@9uD)kjk8GgOb32VHq0n zTv*5@P)5e%CX#ST84;Kq>_Kg~J;1Ut);cP+DkOerJje$LT&^mCvcGg2>m^V;%B!il z+$$jA_w*ryU>uF`)8$H(IW);VBu7_dgFl-UK)yqVB2a9VSB5NS=y6W`#cotm%~BUl z8%a;k_{id2DTrC7v}3Zsh*iFgh_srG#pVMSn9t$lN25o87V48_D^S+IrdfVJm=sl$ zb{c0A6Jzi^9>)K@v<5&6=**n|QKb^Gh1?C9rdaC5J+MTFup$>KK+a)Y&zOs=mkRO# zk5PFq(5q|LB$VM75snRM?CravP?pR^PdcRpP!hkZMJ3?RHpZg@l8+NtbLRt$1N2Gs zgONY*PpxHL*M~>~$(<3tvP|0$&=?(O*L5fM0d-yw>j=+jjft%VQ=iPAa7l^AG!Ymp zT&6e2=u@{JiGs~piNLkVpQND_3Y9g9<9HgJAmz&FlNvtKr$aU_LAV)v;z(_pKq53t zGZV9R#v`B1MoK;OVuiWirMFfq{qAaITmfbsUvlBQfs_5!>g10W*VaUSR~OjA2kNW! zE#_*l^2bmOHb(@tFfFrP+e=P@I70wbGG+p@v!Jblc?!fZPL?3>|k9ExFPL;JF(F&)ZM_--72Q93QxlJ(S=>#UAT* zpw3rLR`BVrMM#2W?S*9lLF{3dz^znlNbl&);}jAm=hc*Fegel1N?7VhBmueGN;8)D z2>_EYkU!YottPH5Ew5QsJKEdeOrXp5k|f1L;atSI{yPC63KDyR3%Ta*2EbyC7}=)V zRPsT(5M=@)wa)?PG!U)jX$h&D+N#x#!K$Ut+9Q|366}os!d49)t`tz*7g)0?LoCTK zEXk!*jMtRa%A_>wETXu42*r}nCC5^buOHScT||nmBHL+yJlrucF`1o4P682ZlaadR z@t3(8)g^uPoBb?;>2*Audx)HJ{9oB;I6~sI4EzRS$TX!npx1<>vyFv3A9z-_lS7-; zhTW3(DV6go!Vth3#sMdtM|IOG+6pLqPh1B>+<@ZPOsg}epgG732OItf7pX6aX3adW ze%V*#Z=89PFeub37f2^>7fQFpMYci7Ok-RSD5u+8qVz10b69n4Oou$0D`U$9cTRH& zo}qFMyaI7qGD0zMV5O}{>p;Vz|Pg`4r(D#Y+YwBg+vxmWsF)i!u ztA zhd6gwX7BJr+jXoh(&Ar9O3doUNgN>kW|n{UQp{HOWcF+J@E}O7qyFeMIXSQHdbzr7 z&+TtxM-~qCvD70L#+3uYlDl*Lu&JV^{oq>8F2w3_p=&lo-x|L7gub2UipvLkgt3DF zn8ar@m(}>d|0;zP>?q=cUNgWqy1#|k_1bY(F|OMlKhw>UzMUL)m5w@Z-|r){?=gU* zWm;*>QE6aEuAU^ld`HF3dxw6QMn$J9SVEiWrr3t^**?T^?k`<}!!;mMXY3oM7<)v~ zjoxGP{z<%CS2Z;OnZklyG28Qei_#VXjc@I|2v4?J9L;mB4Pam6f^9Y;`wePJIM0n= z9~rwB9;U1V$RXe>Zu1~C2#u_chib+QJ5&d2y$Yk#=<^~-dlA6d2kMf_-2CS!tHsUbFpsSE&u9&-58sv zClFA@$=xhXY~y4Jv!@t)abz#>d@7A$4$Aat!!FJWVFYxcl%#ch6>>jeZB*tDU# z8*JMAcbhil@iI`z>+<_e_{NSaI45}b?SeATbZ-OYnQ@GX8fQKOFuVP-rk1OD>-BY!r6 zcIWI^uCxw*38oohB@eb7i9bUUh9pGhGx`l`dD)p7)2KfDmUmHI*y)dY@BcYAG64kU zEE0>=h1oUSiV})Tib1$02@axD!2g&oe<=600)qB4-!2$2=U@X2_hTT2b`m_VBP#@; zA34|XgV8+CZ$Z>s6k_7}y1uDG5?dt>2k0oq#{GUS`gC$MTgm! zGb(FXVvSnywmCcY=ZI)dzWoSh?Flk{^1L1-4PDl|`_7F$enr{ekg{yFvQIeIchLS{ z?K_m9R#~ah&7Luj>Ds}Rhf~EaBf}Rjwuz2`zhW*~lEHua@*H^l0J;r4k`wQQyi>%d)d}Jvg?C*dimlu`-zd2Ohyl6h zdW})vX;qN)RxbEMpgnjb2s=4~e)94Z9}Oe@IkW>*#_xMX5C@_0FB}9Q6_&d^FNgoM z&%XbcU>lG`^FB5dB+)FA7wV#7wuzlLd@lU}^D+HkY^|cEGU{G(=$mOTKW~tdTL&_= z_e8-99pbSu#?TQ!ubZP9oZ1z)#c-dW(6h29hMkDl8$iY9zB}sAZWP1Ep0?pdu%oTu zP?`;n04=fS?BA2zHR!jBQ2kr%<~k`iLTH!jMQ=>U=h9&Y+4AC&_F050@KQj%`I+84 z$Y4aayP1i7y%-~XAN=wYHt2(|C_kQBOe2Bn&X^n0;?{!dkyZ6r!d_#@2)*rnS*@nX zpl!E74^2N%5S2aqJ_(S_gkr9oT%7tf3bysQ%Z;a6pn2J6%02Z(^T)@kY8F zG_ggZ<+SkrLq}XLgTE-EmwtRGLT8Q5WOU~4ls4X*Zx|*=F(8;7P;P(K{QYx#$@>3R zdnp4C@I!A|Sg`8g@hNn+cL}$e>U|n?rcyyyAM)Ajpr%~2dCu3kDRZG0weUBR%kp=f zbQx-YdXE+Ed)GF5pBFw{4krIrMswv{lt3csKRpt{LGWl^T_sLIWZF>umVLb2pA|{y ze(}N-wht=fK(oz({t#++vMfEPm7S7`NzncxZw8vIf+7;SyL=J=g99a9grhZL+R#Nk zOuQ0^Q6S$7pNrTrgvsC4=kzF~Aeg)^e^=Q%a`c<)I@&G+G1K^2CZcy_48R{y9roVq z>I|#!-)&tbxLc`!c;5!R5pwjmcO)9wZJIU10 zZdJX)qz6QBF7XN7PMoA~=ja7csk4Qd`=ImD$m*=<_mHzDk$~U=6}DG=4vVdJraWHu zm7RRUjRzatu+$p>Zx=MSw5TyX-oqVn z6usvpUHZa_O=mWOnmb-unJh;`k8l-J2w&sL2hna#oOfI9-(7FDu7A7}2*qSViNF~Q zSyDDK{arYJc;49h51e2&z2X^3An(e`-WQ$=sXt;YxMpoZst~PNH@j>KofRyZe~JKpM~o zwFJesKZ5&s%#sbz6A;?)Fa1|gOOQX==i5%iq)gosds})jTA1e3!({%Q#hRmLt_zp5 zevIcF?XukaC)Rm3=?9zO@PPMmD#@0B7gi0A{Oery&w}{gIR&A10u=o-0BYz(dL}NZ z`va2B`ey{OV>}W##Uip+qfaz+FUqHRF|gr{Ky?P3Et;b;-D1-|zL1hJClc&#Z@+&^wkKfuK94gPfLy8)3pZyFtr35afkY_v$j9 z7LjgDr}BII8laG#IlYR>ZnALcaL62Y(riZhFQHc{{)lf*?H@FD$SUTZeK~DTXmq2E zndm*-(M;t74V1GVZfOl6FYiy{IL5|_s=&h^@}+Q9khr<%Z{)4M6Ru7J7d8}ri%5W1iMM^5D8n&)!DJ`2#wCRaSW{(pET0Se{Ik@WAvssFzW0sDV* zBz;i~{T;BtJ>0vfD43)j&4bo;z{M&j60f*>Cw2TRHd?k3~~O)2F1Y3ID&t5 zi{K9)aj0C7rc^!^u2;VF3R01gk!fOHZ?rJ3T@*_)X8coDfd}~)&pLakjLP?x3_?Qv z!cl3HX%cg!prV#r9ys&HtOtjkso#LW2U{@04~}bu4Bf;?iPLlnGpGMyAG3QjJI%53 z=u36Q1u0_r^1(49YQ`KDs6$=jXo@PTSNzCDHoCiD-X**a0+o#q;oPse5==}lb2B&U;hgebm$qwhUOjlb` zLHsMLbR%$pMA;Zq9?q*ve}yj-Jg)mj~sL3N{#TFl)E#Z`n1nT2nq zUUi@VCBMnKI2eMK^4I}3WYobC=_;%0o1cdy3EtatkRV> z^g-RF?dC$cPwdQ?S=*0iqV0m!T<0U!qy#Jlo*d`Ic1K7*{%q6@y4I`6b$j|^O@Qy| z;VI`5RINRMO~}Nw^+A{4g$z3->~Ow~ajgqL$DOpL56s_0#CM$u(QWd(O{W{w3%GZu zGI&rDJ?-81jjX{R(+9pZ@Nr{;@cj(Z86tRFP*;EC$Bde()39o=G;*8iPgW$5)_a*7 z4g19QRf>lB!xU!Y^=t7g|z>_)3chhM*{i2wSW-Gy!R?{-|p2JVsxiuCMc;T7km@8 ze>xjf%r;$mxAH?Eeq|M8?FfUc9i%PrS&f;O#UkGjFjQ~_j%Bdx(OSZ_-f~I4-Ei>W zQ#}G_ORAmmI~sHa+tMkb_eS}5`EU8Jk%>lqZ(iOBXyEM|1|q@NR|`0fYtTo5?r0A+ zZKVb&t+UFfDm2T^CW`QqY-I$o@p96kK~6eg=)g|~4QgYqc&~g=k$oxQn9!odjya&{ zABa?-$TzKwfQqTDzQ$1xqA^;&I%bwECv_ptuCkg2N*|E%3=Iq-j$X+% zI8b>!B#6#^)rvc=SJ;v2@t`FZu~GE33AatKxonUL6T*&xS$uFzi?{WeC4>D5#ME9}n7sx^U^{?)Pr;kJu_ z9kIShC0B!^$wZ*8G6;5i9_*))!MO!^#&GKZ5yAK*C| z+9~GD3>4d{h`_^=BWA##G6)e4J|DJ{e0^htQ{nF6kgl^dsv2h!Y#^UbKfvYQ&Uyk$ z@U`+1JO&`YqW+?Ni5`h(4eiTusQgF#Op%&ahEFuavabgIl=n&^w|?6-*%P42x|!4F z_UIKZDSqULjtN>ehh;S=<6Qt$WMUL>Y8HkjYth%Tnji*+LgpK1BIGD(4xDp7a6-TV z3YLppyb)r(OsnVE6lxTF=gs&YTvgeByq*AQ@!1X!8eH>0(!{B`tW#59&WK|JBUk|#mr0JK?Bg7?OAac(ze)m@G`KP+I=BKu z`E-XrK(&A{Qg9HRT{I(y=5vft(nr*nK*g;2fclcK;jNT>@SR|ne#zhD*y zP9Vp;v05*|@G#n%kvT8I(i;Y*ow!PJ=ZKtaSPZm_33Ft_8^CexjR9tp0(TUq6)d58 zQ|>J;x_PEe2$@X|0JF)56@<93O#A<61<5hrmj(1?0ex9O|9eN!mjwhJQD07=FDKCd zW)1gc0sa4O0oehmjXBmAE1ebTAlvIXnw%3@E)<8dc2oft>?<6vR8sPxiUxH6q!RPk zwc+7PFtU>(voh2a5AskD%vYeR?0#`%&eVyH zIX#huF+cKZ`vm=#KApgR2d_IIabmoJOpcIL*;;2T_J;PGv52y1wW;!i(Qd=+4W{le zWU#;REr;fdl?ch6aN?vA%RG2kEc>BtRDo64G-!EqP@i5K!|7%AgmA|Dn^NpqLt6Jl z(#xVNH(oWoI$}NcySA(OBc0D-UfJ^t1N}FMy+IWS(VJ|UJxw&-Yu_V$P#s{Pz_D>9 z5z`tF+K%78ByoR9mC!EMa<_HR9L!j)0XY)9Xw94g6hgR5Opwu9!Y~a1BSfMf&XTs< zkp1|r#2~bgPn;dYxz`~|1|X77Hrc{Z@*z%e7*y}gGTmUGCDG%XdsbRq9R6Mno&1%I zT^Pb1r|Zpv`=eP{8AXXW(Ji27JG~%N9m4rn*mlB76!10l=t(9T3z0^=(pv5tUhLwK zDux<{4p@hZYB|D{H&QzoSyfR&-?0ZU{zy{8hMEKG(L=byyC6y0iQH3zE1~pKQr1_n zs3ubd;8M$M9Lea2*K<{KDZ`?%(J}UFeE`|ut_m^t~S6G*H@5^ox*>szqH5!zLiq(`q{Js(5iqe8_)a(?PTCf zT({DyuPpO=qjaFuwAmSb?jQeH6uak_od;zIMNLH38CS+5RYgr#pzS%8e|dLc58Dn~ z2%NFUQw|RyjS;ws{`2Ws#`lvf(L&%Rn0Y<08yzQNRu-e3YnzP%47S0 zF>z~c%Da1)klsG6ZJ(1C@@v=Ui;gWxZR24Nw*=Co+C+fmINh^vIK-kL7_%%!*SpOZ zrXsWUo}tsxbsKp3n^ZJpcT*&9H%JN?Kmh`ib7z1?OwlmnD3A5r78 zTn;4Lbpuod=!}k*_kmmtMAfz$x&Lq4Eb=fQzxp}XxCi=Cg?P%x$Qp&%;SMu0bAh>YcWF6FpygwHZv$ogCrSHVq0&)}) zRzw6CI8Rzc{xb@8NCX&8TMp}^d3)7A>ycL{b ztY7EA33~t(+K{I)@(AP>8W5j`9Q>#mw9-t`nP^CZn|6Hml6H0^GS`?4UeOXIh`Dpt zN)*QgeB+q1ic-t`r&7)P)ZS@3$kga3224x4)|hgm1K{Ca*dLXf8X=nE@>MeS`*${v zisJFAx{)*RwK9aS%-VrKAC@e~@&Zl`r>5Eor)Z75J3UjA2$`LDPNdCm>grCy~4=l%TrJ>%K?-;?ni z)&(g*C;43DOj9qdla{OmGm#!hXCkH9VKJRhewh32he=u z#*WJYW$LJWr~848@yND&XXW%v%K{_VNBHmb@8Q3uEIhn+!okoafyc#RoL|n|+`gA{ zPWX||P)-JPh5{3WgkV!)gwLlYyL$}W%-PppMX*>Zl?%!c1%pX`$!GGwe%Lxwwsvf| z4s6Juy~6;~ygJz|M`um zENiw+7TIXyVnubeD44faFLxP5k$X!;bjsO-*>K%mBjncW!TBHN?WOcBeS?6AKSIx_ zJ)A9HjShZ&5Yq!R*%pZ2VnD|C%f`nKyVP&c(wEM$yjM>RIekZw>qU~YAXz;hND*Go z&D<0lSbPR#B;Xv3f}wvrTh<8&6E`WTi1F;{T%bm6Tas<&)#w}C=S2<_qe=oi0dyS} zeoBOn9Z(l#!KA~zS+r?ExZdm-SEUClNeNd_f~kUhulyFlcvzJZFmWFYJs+vnsR51D z(4nlsBA6m@D1i2zMu@%tyu1LuNlcRYDQ}bs+WHj3;~uAO;zsYAmlfy+UXdC$2D%yP zPp@_jUQ;f99G&wH@PRi#ekT@V^3Q`zp+HLex(IQU%AnjkFtY~9{MxM%K|OCtam)uy z)$D^+nc)VxE*wDI_pf~DjLbZuE^__HKmH+;6)f0M`97y+q@4KJxiw%z(LKTB`tQ&G z7#sc7{0)m1gziXExV!a6$9|(QeclPveMxniTa*1s@QoZLwl4FUMCoMwgUhxC^nD9U z=8>Q8t5H?Z+HiKnKXJ_k|8vil0@06EO5dE{xxASj9?~eH7qj3j=Z@hn441%7 zj>C^`>Mo}jG9B=ge8CZMmkozp_$EG6_PS~NT@^&IF^_fScf_@&w-Nq7NxEEgB+3?kp{X)%xHe{_kdNR~75b6-$`R#$4!FGqTkv{}GWp=fd z?&&ixTMcVgtzoB*hDY*eNiwW!M**I>!;Fxfx@6dirk$4BlAM^iU;Fax1*&)OQO^<| zf#UWdW{U89Q*(7>3)9dw-fq8*Qo}CRvG4CQif>FgHF18Dc6JQXt8??Qbi1w3RB}=2 zLM~B;AJR4>kCguASbFiz`h2fM-L@ynnO#YA7s7FxjM_rp!7@=<6Z|knD;@4{kv$cB z_ChD-m=lKGggIpP>|vHy72GkB#hY9)#3vz~O4hXAkW_f1x5iV7Cnx+{XO;|<^@tj# zCyOoB?*3}xqB0)b{s;S*?6mD{>fTe$gG+o;_R#E98Et(dtiHg}VBPM?@j!){d{c5p zH4@!>a;T)mT8|M>pUG*_f|X?vd)PDks&i;)_K!pJQAw>x;^NBSJF3+&Eq#;Ww-6aI z;VpJk57NEXguQ*d!~p#=H#c{F?ddcf#{5@($7P4}Jr21t-@ZUCO@qC``em{<5Eu-n zz98#0te@DtGdfFYn+7ivUl|$xw7=jXGJf04FKcVlP@cS;upR|Q(_5^u*C9$t$3w1$&Ipl>1EGua|#&Q(4lx%O@2nZl>H*%)+{syct>YBNbdXd7k((6hZ#hp~T<) zJI$T3*M|G1j>Bhr1h8KjDz8|q{&$bX=>RRD`uQZy73=O`x1|_7pRV6H4hquWYIxka zh4hr?sH&8f@z&P9bB+B`b7e32^J!kAadsW&n4hB~Jz_?1Mu(hx=uw^4)JxQNno?z@ z=z-w6q^pL~fF9{EpK%w-Fc+mrU#4|DTuPS)_5>U&IX&w5tMS)yZ*L)czK&BM&i-C9 zm2EkGwC+XHN#?wm0IrbI!=q193#rcWdsv(;on6%VmXled1+HFUOT<5`E{LF{t zp-+CB=nE$Oc=gZFa&KL_t!9_Amxi@!!R-P<_I~uRe%h{Y^HJ#`2i2H{UoVe#GV=RQ zv%89L$E8%=YUwK138l1Z_x89U|M98$L-npyedk#+F*(_4NS?uHvmfo@bjfurqejXWe(mng=U`V3$3kni||% zvkP{{zC<_Qd~k+cTLhMUVg3cUB6z#2(^Ne=Hl-G!(yoT&JAP#?B)oRztv%Q(9SN?-@vGnEgrRVNmZs%FVcG;RQnSG#8cu!;D?h2FI^MfZ! zSKr??8wm2toQZ=bCHrPV?uV5-N*t4Fz+CG7so=5Vxf5G@_RHsVQ%nW!Hnn>1y`Jzi zc5mJx3fLi|b_TqM&>V8maVlk-q@8!)+Is1qW?Nh`195|j;|f}Ct+2Kd3(DhG zQywD-uWzL{KkwVESyXi3=KY)7!_>byci!_WPtC96W@>D+);gJWsq4fTuC{^RPfj@^ zG(UUExw|n}YxbQq^NC7-iv@G&RqhKK$`jpjJ2m6@Wv~DgcJ>i@FAme=A7Rq?30DJp z&Z$QP#=bGLI;Z|1^Db7Q?lm>GUgK8%_O$K2#pVVNGu{ptbxfugoZW0p?_P1h=b)o% z;`7OH2g1XD9mR5U;LxqtOJlm=^>vOEm}j^OZuGgw&2W|&f1h~f9QgI%m(NCrdtWO$ zTeQJ2CE1F}2JlwrYjut-Tvl3dC81y}BnFIVx#ASq61rQ|^twz&S17BRhp(t*>4^ zv)Odf%+d9pt>0gCK<7{OqrJk^o1O(|DC}(B-=+07U-8_6`E3jS(0cu2lsb3{P5TYW z>JPHMFF!TekyBJo$6o!1lUpJ-Cr?A(Uc>7Dr+{O;wA_pTklkP7e$0jt-6pxU=<%pD^u^%OhS%s)CE40o&^ za=U5c#iaTbO(nE^f3agdY32aVsZ-r*yI=RSow5Ea1Q9_?&YcMHwq`SC?do&f=l^q& zk%_l&$<4$wVBhq)Z(w~1W^^#J^VPQLCu?QL3f^8VnXGqfJ+|KCHCQYB%A4Ot!r5PRacNMaapQihu1&N@`iyC&q2ytPs$vWdSuN>m;rXhPv`C)o7erWOt~dEQ&btqB zAWPjU_66$HsYSD=x4nowbp0Thqit6^U#W3zVpp;>V|S+8-iumy0UL0T@Ozpu*tP10 zZX`U_vO3$Sc7O7b*Ln&(nQ!fo=&kKy{e0Wo;EEWGDVU2b3bju=TE4PA*B=3L6C}bt z4LQ8bMNY+*T|qOvxHb<=R>f5rCE*yRx$VY+I!S3@E8lLdeXVKu$hFb?CwuHx6ZcB# z`eAU?GL-_=I3nIY}TIWwKoD)$tq$l)!l2t!YW|!=Zi|$(6fd4 zI%aPOz7621L8sMp@b5MRhjZtdCuyn6hFyYZm;@!)-yel`X1yy)h_@aO7GWD4YEw+f z`^;XZbOa$vD@KdAXPCk5_r`9gD`z?SFFNx4AbR!)=f?~Jg`N~YI({;EUTwWAk}xzd z^l)nZbI&0(6qk^PtE~7=b3W&EZHk*`?1%b;{rDu4UtNEY zyWTu&QRlX!2_ed>Zi`;k>}^({8>b&^3XGlPEYVrp8H)Km*(D0OPS0J3^KkKg$=y^J zqVqtc6U{WU>t^TR|6uRCx`EjAiWiI0pl&IQ4upnP>N)(F>RHbhekw8mxN7n(6iTH;c}x~eG^NyTet)n93c z$eUU7Bd7o^`d&9Qdqf-FiFR}=`x@)QLvQbb$mg6TIh^uSb%(hUmM{A#Quq#A!b6KK z?>qXIo-V9v-!~D5BtN=pOeMwlPQ_{Ct83}Z!UInEgQJ&`W3`Xi0+VG}g$^0nk{=~B zGFT2;)CsrFYFd3?;+&<8S`MJfqS2Rrs&cXD0;x{S1I%QB)F)SPDu>Fn#|8+l#oh42 z+5N(g`R28qDCge`x2J*YZqQ=9cBmUe%f(z`Gt4Pp8nAY`{2)B6QtYLZAGvR$&T}P} zJ%SD_#~7T#t-%fSu>Ons5cWmxq7#JJaHE;Q;GZs(9Y+Pd{PgZRrWS5(T}SQC!RiO_ zQgo(Ks1&+Y6%7yl+`yhV*U06l9O>jR$3#;5K@uL{aR?XpMK2ZQKDQ1hbUC};Nx3aWCePHl5GL9pC=$i$pTkje}cQc0t+y1oh!UD?3bCZ^XdxCpqYYy}ogL3SZ7iWKIqBu?`+TOaE2_Upo#Y#~B3UeAb z*CrT_u}EXuS~SZt`K+|Y2J#9-0s7Jqr~&oNkMkv3N*))d&|cEA^0`*r7Al8FNfZuq zmHErC=XhwV_8WRPMaQqw%dylIgK`CXxYFwKQ4-6#17$G@e?=DX&}s?!gSPWR$Ai!4 z`X+w7|1qOYPi1NYVb9U=GEw>+XKe zHV=cIlgKV&5rkBPV(7f%>*3$KpE>ixgl#br^6NqmitUv%^f38L-|&Y!-10~ieEM4r z+q4Uo#OCOyLE@3c$4C`(%)WZVPoMV0$3^lmS5k!ndB^cmi`1Uua9bCb!VI^d$l*PX zLT_Ass~d!ZL&uy}%22UBK#sbq zqaDip5-HV_IY8NG-kcQ=;gSC?!lP7s@QLZ*tg&L~@cg~FH`(7`Eyb-e(DR#@x0NM2 zn@yk0Y%dH(5TdteXTsXSEk3INeXI4ds`2oS!ckv1w4Kajuv3+Wf2}`>EJWQ`S!|o8FOJ( zaenbIe1gTSmxafAbrOnpNbKw)z{->F0L!>958YoDh@~&eF`2mM*uwG0sfjzt zPNEGp*dT8y3xSx&k0at6hrs$k+!DtSLAXm zisv}uzCb(-`?;K<^HgD@S<`F_X^&PqxRoELXZ{6$LHqv;AgFD*R^5AEJ?Tj)eg$F0 zSrI3jEM{rU(*i(IEJ(#fY^@;#2|lLrXSIV5KSUKpqX_A}?exND=K)RqANMAB%65NCY!M&5O=|!;6ozlx^yU-xuuZtFAGfF9+P5CR&=9C}BjS7;5S!fGe z?`iz{&{%{mU#ROK{KBTvK4e|z(2H4|X-1BmMBrd+TVXT;5ah&Ik?Jc*d2Qqb-qjVN zHQFjicKq!W)CY77hE|$DDrlPtf}S+_%W40jPG% zv*+~GZ?MsH1oi+dJJprPGdWNxovEE?`ZcL{uEb&Flc120dTG18`>JCwxS|*ZFIYOK zJ@wd9?ZuMrZ|bPr#<}Tr?Ovn<*F>!P!XUhxj}I@j_FQzTmoT?3oqVj6&nn;U_2zKd zGTh%@HAc8F4Z&d{H;__dZjbbZk%#dtPEKM^vkZM|dZj!<=f^*L5up`h!$0^&uRU2ul>9Mq@=11cVS{^yuetF(;6>(>z z)v466rNI1(fbENf5a~BUeQ(ze=(GxXb;zGI@r*arenL}Ox>A~N;aTw8Ij~XO?2mU<&6w! zu`K3PF18SYYi@Ul#rVp^p(0#!{?xo4AG{eY;!O5(u2Z+eMLGGg+uFoy&BQYuXdRvY zUER)&$w+tgVUjk*I|hc%Wd3r=n+4u7CG^vtrXH)kCulUhXYoO1d(5_Ex3*8vN?ThEykabQ#QvD!#VLJOgN-g~D!!T!4CKVlY7~ zWbO84Phh2yY6JOToIpeEyX%ZH1g$bPf|si}yRzcJ;TRI3 zA+tYtPbRc6*{tV&)wCHiSs1r&P{_FE5Zv=P`B63=V>X0t*HJF?S+cOgLQ$MmWeq(b z^Q}uq9?@Ps%vJAx+9+ICuC(HaG(qc!K9B*b35z|@OAAEUYcdhtpyZ9KT1x?f?D~t2 z0@clL`q*@J$F1rfUJcD3WwP>sj_EI192 z?WjHISM$N0>=KGC_le`%h-(*k<6~VU-aYzvf+J8Y6GPrvI51y9@jzJUMO-IUZPl4{ zLQBhDfE&t%0l{ZQG1;=t$sV{6NG#niH{0&~a%G<{6k}`CEcv%!91+U8#&t;&e&IjU zd&i~Att@RC*rW>^aT@5f@bF)hlVckna$ipWtOE(fa?Xas_KlxRGb}zS z@38vrx}ZYD@3V2Mv(!h?oB`-gRy6d!)eRED{I}0Bau*et#KM)(+me9LN@DD45Rd$N z!K<8T{+Eu;n9BO7lk$!2`4Op^i{Slg8>GpP21j9B>A>cnr4F+=aN!{{@@7=E3Mo9y zAX({hO^Vo+1*SC>heK8lL*Js)5a4Fg)$O(`OEDi0ju21LE}I?eR7+wbs8|hwzv&OT z645SA?$ML>xAXhdtR_|f$^x86LMn9*H_$e}7X!d{d${z8A19uBigtk+=t{lOK(*&n>|+#c z@+dxMiXerQ*)MyBGG<$fF99?Zy$F=5ZsCc8C;H(xu=+T-NpQ}fHBkR%YCgElTk$DN zwC1Z{vv1It1yI3^rrGUwnDdkq%rO^&^E;fuKJOrgO1Ec+Yb6NCP6*alfrpQ# zT>#ig(kxvo{1=j05H2zVC~0J~SWYg6e(uA#Fwc0Iwr`@eE#VCzqkApdEs)5#q;pOW zJr)IQ^ix5q{t;j zHCep+`kAc4SjjBwGQXc28i0u+R>?9VQXYPL&RL7!FY}Gc+^uo#`UZG0a4r#6D18Kqf^o&gT$sC>PpvJEihG#G&Ucg}TFzr;PO+E$?YU0b0h*Plfgh)QaNx10i9 zh_>9zpRX6s+Bwe7d`erLce*=A4v_zP6m2jXbL>L5D355nTkW?-xC(k+rC=FW5Ch#0 z;LU>tkTy7MRZq8ra%;%;yDNcD3K~ATO9Syg9pCM`zGTqyYU9~vDaRf8>;E^br&Z)QGY=-%z9xsECz@IWIZo?+oi5p?Z7@U* zVm?FFt`6Reh~OM>4_I4^@er^?Nh62*FZ$7W17U+5-31=nS=PuMl7gB#pd{L@<*-6# z4P8!Rrc>qbWOE+C0#RsKCq~ElQJjVz@oxP$8c{%HoTFpgvOrRX6j3rPaU%-(Qnrsy zAd@bfr|T~paZ)oF0b;)4pT&G*??uiSe{OA@GVUTc1qZg(AyBaN_qZ9KTRQtiC^CWb zqKG!=kd;FnG6!7zWT+cTtarO>7c94t0(SY~U!}MU(jv+5P_-R_oa-(ARVGBsXKjx% z>SEZjLDC|~V5x3xI&gMJJS_k-18UqkW|>dSNYF#+>HZLFqE*?>^Dk7a5-I5ji89ti zu`AAJW9w8k{5N!>3m^Y@%8LHT~}HFgG9LmJ|ybzMwgq0Fe*sNHtMQH1@U?wPtIsy)oTp%Y1dh1@{Qz*H*S zvSRXJy|~#MB(!+j41CM(=YR)69g&B{w<%9s27zsBYf0hp+;Bq&{ZpsuKW)z8#dW=@ z8HdnOy0#q93(;m<1g$k*<=w|Dzd>CDa|AWmV@O4s_pcq18M9hbs3qdckj0yd@*l66 zthN5d*Vq|*6FDszk6fj2KkG_PIaH# zOvWqxY?D|DZ4};`(x`RMD*#`Ihd3@KU%1buhFgc4YaDbs_19lj6{jk*KybJq!M3$G zzgPf{CaI+b4cDQk!S*A!uO~Y^ejl;?k|#sCh{0a#-1wC>9Ww196{-^V#PZ;YE*;hi ztwv1ozC7Gm7q2o$m8XRSdDz;W=`>baV z|Ff|Hr`l`XWrl`iM<}VkK{`7TWp5d4|0m;Izz#%%6akK*WAm+h< zHC~>V`QgfCiN|b?G+Sc_7?Q^uB$J!*Uf)+6g&uL!BF$uoAOWO2TVNH#HrfY5O+qIm^mvZn^lq$ilD`zV`(hUkKF(i8+H#a0e&SH_|?LJWSNNtA2=E_b6A-A>aFR2R#bZ^}J+c zS)BRc4oL9#L>!k-K^SB>N2lfGbIMnM(=1&NZZ1>-CYaFzQZ%1)s`MCI!q=kDwPP53 zIq?AEft~>4g~iwk2q3o#<5aB+*e+wfP{S{wW36eaE79hh@{v{ourETQ8XFI|MJ74y4)I=jSvg1K3dDXO(Sz zE1~le`U>`U4l88lAE*elQhq7K@KC$G=DFOUGpy8WHDv0C?0168&RR*8<)*=;*uHu~ zsv&Y~iDK3~lD1w^+3ZCo(BffY$!{PH^+?N$)C>WQvS1*WRcPJ1Sg&(e${HUiWG}&P z?H}Y#?17NYjsU6jHj*hRd$*S(z#%OC0voM-m@C}twCpti=cN&N=7siG{*YcHMbVAv z%4^+LY(kr|pF_7g(9b|N!S$~QI{-a@M2laAs%nTFfdAi=PxXE-E^&1=QTK8ee+O=q zjwB5ElPZCsU?hh1{~B)ut@kp9-`QW*Nhvx8kDmJYSqU#&S6Y?k8yL{1^J5}`-_>=chhpiZQ`Zu(IC>@oa9#f*d;oHwi1u!XfQlw-YrXr&O z>X!L;rR}tD-tFB|?hm>Xx@>tBSN(}uw>}EAG3HFk!#2xOt3Ijx=-l+<3$rNUh)0 zyQY5Ge(P07HCe6~1bc5h5G=}aUU4W-0POqwxS$;G`K-1PgGv6_&y3-&uQF>U%LEt3 zyLjup$0D$4Rl+DXA#ANMD5!-w8qm^c>LP#(rvI0{B?;f{ZrE)%Z94B$Tw4I}9|k`P zx^~-~0)59yzfte}H5 z^}W1+rxCI*Xj=r`C+~jw(A&L?ioupFKiFmLPwevJfgoDP3#gt?;P2mg6Y7`9R!ezc zgc8+Y={`~fyO)qZn_3vF=avp@OHM{e0)gWXi-Zu_QEB{%H`mT5d?JFY(*ZJmQPF)) zjMLH7p5J%DwbWqBgYoWN#}{UW#1u!j3aI2au`9?RQHyAQAg%y!2C+x;Pk3Oxnj3fy zmeC@`&q0shZ|%~#klV24=;r$g!4F_^1?pa_jH9YO$8l4Zk95&P-N3yCzJwiJzoWfg z8Uk?O3u-amB=BbQW$d`;QQzU2agUZeo8Q*(pYQ00*7v?iVttoohQ3B4xvE!6D3?Yn z>*czBJs8PwhEBcx$$q&UhK*V9!yXHsr^ibyC^=P0K@*!OVnAxzk+-N~m$RsX>g}FM zVF%9};U8WkT(G@58^Q*l33|x3^)qt?9-B&|i;x-Q5fCjJ)-BGx4(gt&jP!pSDui~7 zRx;>R2M}J=O3M{R^jhTxbG%Wpiff2RzWQ~G{bID@aU-pb%9ad=ZSf4qYnI_6=jWt1 zP)4kQ&B(3!5PU;r0pCW?!15h@Z+OIFIQ2oX+ABU~v|Kf{C(fLpB)fbvLH4*l8!#!I zMgbiK?8<3QF&I!fz&SE_ypTj*`>rlZtSr;|nQ*2n@ASqWI`KA1G*EIOk6}J=c8<=A`)$V3z?InG!e2i$PIpKT8 zEiZLAx83kMD#r$~TGcSBM_Rf!NJ<}>hw~v3=r@K;9U5NE#BN*{D!CT4NX3Q0G8c4Y z6I4-meij+Tr)aqvtb7E$=3;%Wz18oJQr0Txj(KBbE`Yj%FW8@*R8Fdo=-9$g9f0R% zph*Pe^)sMnz0<@oO`yP)T`nV~KXv>&Lu`DMFpO224I>vqNQ&|J`?CItx%YZx&+EJl&IGGUj zpeB%>A*wq5!jmz8klpt+!VTk%Et-lPg< zdrUOd;}-uy`>eYK1=inFEAXmo4}k6<<^p5#z+6Rs-f?6&IPS}U92w5hf~LGZZb&_K zEVyGn#Que{08xyM%dg{TRtV4-L6t9gdcmUKu}caqP7!Pg<8Y<~b@f}s0qdZ>=RGO_ z=$RMzoLp#W@K01#`BLcD4^nELhCU|K6;3maPwv@AI}lErE5O5`ceWs7qvMw5t{^Jf zI`5c)xuhbl-SS`ub@s);gy0(BB7i^+;uf)aHx%H;-@VSu6mGa6*q{Ri92FC2E9i=_66jBVo9cTEEX z4Q#=escLY(r*dw(<+LKGcIeJwa(-K6=QP(Zhm%U2V#3TH9< z8EqfuLr%KIM}e5oTuf)n>_uoGJL|{+L)c zQ}0fl*Bz%7J_{}!1n~meZkqm62b!qAQtB%f!=QJ#?0DLwb&;ZC$E|4Az`%K*j z$L%hG_4?glBML8iw*6jjf(rNVjF_vjcF_M+P^V)?v6cI6?lyUgbKpH)!tlzo5RvHu=HqlX}2_LbAQrr}BD{ zLNs@dTf%UnJ*Y3>rdrRPVFgxp;N4`A{(GSPi!cLrJFD2b`|5YJw^+H|Z8!I&E7Moc zJm))&bx2%pn3=2RMaVfaTY`YZl z%iptN3qdB~y@2ZmB)BpfH*8y_p@+X=aIwrA`u6uN;Y<5&xonwKe8Hzrde%lZ$gu&_ zmHo2Gw-p_ta3$=$BZA%g2N5jhKBrhQu4~?Bz0&7dzoc4UW5<4NwKBh9Y0%i1ddqVF ze#56LPl03nWRZ$Jn~*g-FtD(Q1Fai*O&hXjxP&^+5j#%q{fFg%N%z*Nq2AAh+ulN` zhWu1-iB_ovp{j8nXeSoaJMr%^T{0N$iLU4d*4-7)RsrB3@He23&EDoaL!?%8x?_1` zjAMS*8S21gQ3KqTe|EJikcCxcPb)6L=KY+;X1*S_^VpiI8;DT`pv0H+3BQLh9@#c5 z%$t>z^yLH@*hOa1Lf%2AEs6GG2b=!xPN3yCunc~DkxEHv5*^i5SZG38PJ?>?lc5S& z!=MkISYhhA0hJx{4&SK*?`XnxA*;A0r8Xk+kNja|M6S?);r6O z!uyf$Ha0$~pJ{Oll%9L*GY~82VJsfw&`kcChTjmZ1h^&gvX}Ed5!>ta?59FXzS-{+ z^sd&rhzD)b@b;vhtj_Aud&YgF6!%ucZFna*Dxy4dZMS<959>uO*BMpYiO%F<`wr0O zBs%trfHB!YJS`itRKNMNrJA@}#0vs?{U*JuOa*;DyC=rYR@_RGctbpZ;vfIbndx2g zES+qqd`8WlwTaDR{}@02eqg;I-}@taMatbycmt2#xT53P{Q$2iym8HZYRAA#B?2uT zR03{zS!;wrR&-6sir(+KXUNn1Ux=PWpZ0r8@(wwu$8sBh)yTE8OIkZOMtvc2j+jJ| zlp|UUyD&7{b`*ieU)yVcvIEoe>qq}kzu5Y>E|qSeDiv%8Gc&G;EswI`3q+`sXa^iD zR=*f*ylX^vTD`?(%_ADhqBUX`Rq?=>tFt<@D%DRO_8aYQXCiQ&5%=N_XQR>(O*^Iv zxUaG1G!1U?s%vLI&1Mttx<{Siy%oP(&}<_aAw#QRF{-Ge5LswM^; zJ|Nn3XNlrjtT*Z_t!dq3&*io+9k10}TJ~ESHjb=k9tjp$Z6zjNeo8hao23JNOIC-X zc{a?*y4QPT)M=4GySAa|c-lb^q?tPSJF*Dr8H?`c(j3b^F6u?r-)vae%`oUiiQF_R zvx)1g%M^^}MM-<$kKsQpBtLItLr6b?ZH-l_U-Tj@AY@vpmIpsf?|LP~vhTq1q`an{ zkXnu(?70yD%!XHXyb`wQUJG2oAu&EF`8N&ToP=MnLoxcHl3%X}Q1B~u#JIS3Z9Tu) zkc^OW9nkXr)G`rls3Pz*#JY^j>uXTn)#X?Q7kY0HnMl3OD6E{H&^fy_6QXZtU%L63 zQ~q$gLWfGrLiC(8sv>Jm1ycAx*}_zBLyeB(f6H@Ruqd{_B$yye1l?H4glQO${QO`R z#)(U6CdM{-YqbyCQn_Z}b1|mWGY#$3sBbVY*OG^j4BK42XVzRPzV$TUc_|(ZE*IF6 zI5ifN9jo0|`G!;8MuI!D*Vn9_13DC#m&f^1Z)0Un7pAwcar&Q^p{w(@qzbVP2Wx)F4*` za3_24S)?yJ&68W`JgRQsob6ejHUYkgJ|F$uim9U?r_TGNd=7|X_1CB!5vRPU8)rGG7(&Yi~ zRTZ>`eua@+WEjuB#`?r!2X_Vcp2yGb_A1?uzD(o8+Or*5jNUjF#beN?>3L78IfT5P z^Z@Suhh+ibcdiv_c~-M(?rWday*8pD>;HVdtN6PREE*s_$%Q%D*?x8@gHP^RkM_<+ zmnA-$W=dB+l`URRJ}=6jUjU1%+UDq{l@qjDM4N=@H-5cI{{GX|jP77|q<}_=t2cMK z^~k6E4A=OBrKj4H2@!|B!?$AjW*Gvb7cB(z`SO8R*1p?8vQ+{(LJpw$<-MQ_(=Bb_ zzEO~$%wT{)AjKZ9Fa9Q-F_uY+>7T15k|SW}d*J4K89DXk+j-FLm|Z%n&rrrQB4DAX z%j|K>^*4{UH1+)@TF#saTDs{&H;ckM-C;a63(@B*3UMD8XQEr%S1+?rY=C4Ku;5Qb z;HduH#qQ@3tLQVs18_+wkdXCfTIyPCH`=q4RW*lWImIFiy~nfW8|%BA`X&0GFIJXa zlv*Ri1HHgQyL9=cEk7*=`Wf>$8UIys3Z7-c<_5m9(pFKy-L+O`_DXq_ZRc|&uLXBa z9il4~rYYGR_<*b@>FP`q@g;}lV$K>3IaYX2;maQ+8n_Dy) zb!aT${WfwIo$f;ay8q*dao8rI|DN*~#}3Du721Z{140cR^!v2mfpohubNyjjl-x@i)o^@8=X^B(f^Ft+gt0SwV{K82r0jh*ad}asbEE z<3{Ut=TV-PPki{r@xvO0AX?{T&v(m<*p8Hh0F1-E(FekAmz}}!g3JZ)?QJCU)ggN$ zaA&--`K}<^}ODlg-z;oL}fI9)aH-qTA{GzJzR=QsJUfN zQ_{xZi!1gZ<0;I7`bLr|FT5TRZOpQi*1me1I}5H2#|H2Ya>a3S`FKYrCCsdNry2+@+;)G z&03d74vqiT4|kwG7wG-1z?b<7vc^LRMC}G;7Vt7X-JKuKk%hr;kO&xq{^;YPClI4$ zsC(nR8?EyH>>TkAE!lBnlG>*EUR7y%pG|{`80s>WGTk`F+OU!I=C%GJv5n6X2#JR( zZ4@yVEGF&*GIZ23@Kh@W)}Go1=Wk56O*2CqCo>`5@xkk`hdu;$xJF)Hb*4w5tf&AP??NA1O)P@LDrNBo6IjydV2$NfY znzl7L=O51PFT!DZ1^;&-{X1a}k)%7dY+72pOq;*8`h70Yo>ggJ%YUuKzpHh64rzzmE?bAkPV zPpp1FS15YEyrZGky&)UQj!`xm@#Bzh)00B7Wq+9hXp}xS+ zo;mCZc~lbCXU5OTc%&8|U6hSb4P;ot*a?=;e+b0C`&NUWFUmTzMe5IRicV4+u}A#W zwa{y_#G_qsSBS{=h~kI`ks#Sg-3kE|n&6SeDUT{ouGaY`mD+#Dfhs4)8MG;J4CM?% z<>G_J_Lwa4q8tuk9)crLf1UCFfNV?ENox=m^-5tIRArQ3=rO0T(@#EeH} zfnU$Vp|rsD$hl9uUI7U+ny1)a*H3)eDW&H!goxgk8 z-0$YWrh=e-anj6MzhFU1q|b5=^m!*jfTb1y8))HxvQo0o8C=c`FMu*ic2tkHSJ#hI zY?1x)P{I=PJMBl{EP!IpM4xq<^}1=atF64Avt$?0k0yR(?3NP#h48}W&*?V#tPdYj zInQ7|DREA@((}tT9lbKk`y*)fb>&sU04#bFyL?6%U$xX14l*9nM~1B__2g1O3l~}_ z;)+G`6CZeduLsmU_Q3-mZ!Uk$h!j}Ek&y>lTECGnKCZbpGJgy*@3QQgcl|gGFURYr zkdeT1uRn>c^2QvO@9$i?vFZclX!^>LiTUx$;?sOD2N6~`O1Gw`uio)mnw_`x*^t5C zwAF?Qigl|t-Nzb}#(?M)=sic-?z$EOril95hlRTy{vdY44{S@<7GE=tpY}FVg|^Iz zuDg~EgYV{HFrNJ~IfeB6&)hlYk-!xL)?n9eBI@{ST=-WvG}t!@a{bqQoN)$8)7Czx zW8CT`S`Of%pJ-frojYbsbKol8dC%U6siM-K+m-c(x}y>Qp?9 zZAg}WJC~JjIel-htg}15d7fXmod2fVCKHzHczL6GJMA8n5v>-e1&C0)i&a1e->nk6 z)$ENN=tjdsZ%Mf>J+&Gt=SI1O>}7gT2$mHD#5ia%0(bhJ02_fAqvd!|CMIx^SrQWM2Z z6REvlPqhQT8K?Y@OBdV`{X{F49!+rw@y5#_osJE&F6*ckm=@qnXry-r4_eP3L^~K3 z$YGCZB%G;d)CIl|;GEi4i%>7UcAQ%O6k~Tj>U){rhVA}f|AQ=7i0=R^yy?RN+FgHD zJXEW|Io(-9SM)LIoR<@d_HGT}Hh7AEZh5tX;XGZroaGr{Lg0te&XiD7pE%_uNc(@D zVF31U)Tcsnw!IA}kJlLRZtB^rAB$SPSXr6`Q(Jc%#Rev4Iarc1J|=j4KYd0ck^d%e zk3(nSDTC59DERLDP(bUb9(}Lnj$qqSqgR{ITiLnB4CF$_GZYOqM9BcdnDU?Ip8V3F z04BQyrPoTmWEtGaDj=)dAC*LZ03L723r+>z4oi?gvd#?-=5m>Y zid4WT3L)j>Q|f`s!}h8f^}Q18Q%ghBMx0eD(%+1A!lbofdmnWP$)h3JmEgK@v~uN% ze{%=AFDq&PiVBM)y0v78VA=d|C8`o^>mzn2L%+={RUPPh!>tLsDwki@4=pn~XI z_n_lC^!e@h#!6qD=^7nYS^aLshsw!qgnYLJenI7arQNd8;#M-&YerFn+sVCc&(WVf zmp2B~zCYBZZ7v4ZTVOKX-g5HodJH@FrSt4A+xmSX`-%Ms^jVw0zeR+0F~G;WS$<*A z??Sy){?6E#ZU^IsU`h@Y3U`1N{Lc}x-Q)x{w1eL2L;L5lBX9-*=@LceJKXwRe-xFB zmPh-{f4gDoc|AeUOYHP9caFvo5ZD>0B&yV)-SL|vU(dKB9hCi;T!=Ywu@h>_I^&6c zk5&lvytRkM>ku?Ss9)??I6<7tK{2**$LB=(jWWGLJhxgn|AcE0^MOKee{%P^fbtnc zHMkF$6Cf?$E@peN9}y2}f1tDdPPpOD*T9!q5zuZ0PA8M{F<6`ILtDC=mBe%{wC1sa zyQXL=BnV>1+4$+A^Eujx=5`m!Kj$+5%>w_syI{T#ugsI4TaD*W6Zcvv>ZdO>7V(Kw zzW($Q(tAb+9vq)zu~zeZJBs}VOo3Ql`qUqN-o+&g%-?|S#Qc%bUMPZSEp~3F>!a|i zz3P{)59k{2=6i{Ei&2L>aZ!|^Dt`3K5#s*fx6&eRl=!s++wHL@=ZxoT?c>-@ppFO> zkN=}wBlYgh>ii45Ongl}F@QWI;UB2fMGUfNS|6)MK*KuOw zoB@Sk&NR;FzfkvyOL>3}`l!7xoQFnNf6v^d^I+Oas%;$M!GD-E5DmHx!|ujW4d`F?@CgAlbQ7cuQCE`eP?qkOY)PRqGd7TmrB7BII0 zT*15Y#eAS0^zH2iTAlZP_I}?X+5Et2X8x1)bOzsT>vK^_>6n4hEH0B?z4{@~Q1n5kMslk|Jug*-6B};2U+K!Ukp!Dt zdM=91Y`Vc|0V~G%Xmgog2haqQQ9`csz7s#G zu;Z3!NqVB%w|w)3^xRr?MfrlOC_QDFf~}a% zHbV#|74uBB1;0JRr7UjXkGSdr8O$(0Sm2--6XT)5xte5lMW5v=pCtTO@ysUXJEPWVGYeft(iILHIlw_njk502 zr;_(LyQZhNyfObeB00U?_7v6wcd@4h$L{u~-ExRl`~f=m(WX+w=fp28{Tgco1!fKw z=|8$RE|TJf2Ulnr^q*%*OwQ+S@x6^jpxtLG-p^UQ;Y0AXOvnEw$kF4fu=>2M; z*_*tXw|s00S7c;;-qCccSXD}`CxD-K&XT@JcJ%)3W@o{(5!zcrV^yS|^tyG3@B`KL z!|69p%h+(`?f2?lPKZgpnT-wa2RxoI>K{M?kD&mrMKScd>n}P#0sWC+ z0Ls)a(N<3xXxbA9^p5r37>%}r1t7(=R-S^qD>N%+MsI<2LC+)NlD4^L3X(jG1q_L) zpoCxYTEN0okgVIryEuQ#cBo#V~iQ zJQuUYW;}@>G8_V7HFqfPvxDFzKy@-45Mo_e%ghbE4mQx6_!U*-Dr7m3q4E^fn z9|E&t%)?%;MLNA;VYo`VVW*cOhwlKr{x0~mAH+?z2N_16xfK*wNCU)GkTVYfqrsR( z4`DygrojNv`*huT6t#Bc0ESN;>=R(`M&@MQ=*o@|WBXS5A3#RhFu zulPQUeOx{MFqnCghE@P;uFg70epHX?G;mhz09Q-jHGm@gJYYtCAC@~eiYzj_Q-KB- zV=y5V5zI04sZ-kv%%1@XGVUC8G?b2;(6kO{qAuuHwSa8%3#orI&I9D$c7O`7>e8Wh zQmi+6vAeEhBnMB7l&7edagWq#;1;eqsEF;qe=C?*pM4%y?ET|Ylqu}N%-)tDdMMS1 zX;=E!16!L*&6+NfwI;rax+7QHofS_mv=@Ad0$yi#AKZ=rO(eo{!2sNRC&|B z4#B>|a$pdjPxfa~`v$DNF2EqgF%iAx2iL%OmE?09-@zpVX38mZScss2Zm7LMa8={s z0WrIuriBHAFe*qCkA^9+^n%kR{qB14lc%7(wgGqIG6akqJV6i@nu8R)6s9o>Yma0& z?%{S3v>=yqftC}?2}Jfa3PmWgo0jo`b!dbRn)s4BM=`BkzI>!ZXskA9_B^^RNT`8j z4b0tux;0eD>wkwyfg$NHan&eAVQVn-1<2+ulOUn0C0TSD=TtmJl+jtT4rBnigev}s zbWRgLBec7Hw^zN*=du9%BdS9oDhV^pyflw#phudWe#XRuq1hfm6HVLt2VAUr0mUnN zzJfp(unkM|wxGf2fnk8IfBH<|G=f~D)w!Ih`bTbWm)s)riGMT09`uY%;*7a7Z^N(K&JKIktcv2 z7_LK1`HXOH&|iU5B^?m-fOohGSpNWjPkxRc{E5m$Ivk=~smT!Jg}vhNsY$9A+SQPN zopt)z9Kzk3__5 z)S*b!TW=32JQLG7wN`C$Fc3!VEqD!rCIfOUCh2Vd#R7!-xV6x%1 z`OJV9sEKk#A)7uLT^kQNLTk)3z&^|T5@FN{hG1=$=VBOh+JLnJI`H+am2HLwJIPm(f{WZ1)M@# zb|S0%12T*KfXr`%4IVod63ot-yv14Svv32!2D|cRH~I3foM1$3x{tV>Z9bRUJN~>5 z;EE72BL2^|Hyu#0BDQ7viq!FaFE)I$-4ZFfrdASyfbmPTK5AE|`d3cC|CSq(5#7uG zpBw#exDjx?gWq=*ym;~ASL|Cmbp>b1-nWg%Y>{i<&txZBR&IO|6jJvLsOgDf_$Q)J zX%r5K0$=NfJ@vtO>CM3Q*l3%VC8btQ^PGG9AIF3c*-r)Am=wPe41r*m{G7}f`9PMO2)T?BLITnOn%6?!Ezvj-&^US z>E(U>t7lHq5zAl%!tAHHHe*XeG*(JWQ{MOJou$C2N4_(It3O;?aW0}FmLw##&tn?e z?Z1kGIn<}^<@EE6%}YnVFC|{o$UKLo5PD0`+mhhLiai@G7o(f6y*?_(e$~LoMdC}E z4mOFeda`8RX{vZNGT+cGmJ^TW_ewGNh-x@l!(6S19GP>^Yl?ZRXgVL7R&gD_iDcV%(y5J-rG$ zL$dXJ&cd5R{E)hZn){pC>tGBF-aiIM&hAdL&RFPPzOVc(`E0yT%gKx8a}SH!D%`^` zSPTuXbq9l$LMUZ)0$T_w6TVsfAma77vPAiW4>ZO)nZ3+*ri|IDPPK0SZ3@{JtgwC< z%nqad`jZs@#MyNlMY$VIp-mh!elB!9w!7&@N?(%Wkxpz>btdgwqN_9aE<_Zzn?Xns zSqP25fu<$Qu99FJoBpodS%$=-^1ap;^X-Q)`8Iu}&F5hB!q7ZPt1?_-5$H>nt{DCX zzmD4tG7^X2K=bcNYBL{lc6FVQhbD%$iVc7fZzQ!~K&E)nQ!$=s#Gn?n25f}qE@zj< zVJ=BcDLJW*{@2$vm^lbqzQY*60QhA>uN!RSAX*vy3Bgz zANkJhT(9v~%r3>V zfLOod$7zovo&BIk8JL4{t}v)CU2_vOj0GKx=U|Dfjey&eChb*27@luc5cmoGW-d6&%%Ea@40wTh-h}sJr%h zDIjPKcm%=hD7d?i`Mdg(Xv$vQ;WzNmuaRG2Q0gHzr1RBzn9EkCPPz*L&!Pka~N+h zy9o-*S23#n9Wej3eYrgm3F0qcM*x;`Q)2y$FnxpdCo#6Hw?I!`uYL$dVe@z!`P~Nd zIOZv2q20Z`>Iw@SDnX=xAR=2&l|2NFZsLfMbq^Cvz-$$fPKn}3gdpq9_VD%dreIJq zLAZE9VU5^@pEvuf#{ZU6esQ`hW^<~av{|)bS~)N&;&q+34fjT`n;AX1^LcXogDjs> zO?!lX>l$qe8MV{(7whMyzc#FoVp4~ANl$U4aS&%#yXPH&SiAhO&p`iH3xEh$<+~sK z0K%H|)N5vrY5ky`kI3jiV-$ThOJEU#@x*Wp=t>ZWx&05bFkHZ<(ll@r*ce;!!uvWl zT+ObScUo=km{zJ>x8%6SqH)%R_958BM|0Jj`%yt60W@R3zcsqme2tVu!R52mV3M?Q zjDlG9_IMZxdDI36jZI4F3ZdB-4Biu+geII&iO_&7Z0kE`nrefO7fclM@H{-<&F6A4JX9WLqnB_{>DKq^2j4hK1^nT@5`o zRSOD5$nRlo@3rD&#kLX)@P%iD7lc3_6N6qUu)HrKtjDD%@ecZ3g?l65dIZ#$_)1}s zXR8+CNdokBhqf7#uTV_^P?}sx$>UYAr1qC;C$---u@je9$@2aZ%!SH+Bgm+3{CQ!O zr~4`>Qb#Z6_5cq;(JV*$QWja58G6%j^)0D4aBdY##N^GIwfNj=LnIHr1nRhZ-pA^P zRUnG^HNw97A;3Yv4K+c_q9>7}QugH;ODTae4@5TkBu>Lzfvhh>1rCR}va9d#R7}Gn z4hwaVL+2DEfr%h97aWouYmy)s0&`>1@AFG_Op2p`9o?t~$fqw7PpPOttgv@AlSF!U zF_FUV_Wx$DYxX=+1;^Cdz)E#z=E~ma8N4QY|MR4zq=R-!?7T%_lyO0ofAiKxbx89)KifqSxrK|5 z*HN^}*D9S>C7RV{=VDzI#%EW;+Wq9Hlcl`~0r}}#=u-)wE8CdUpK7L6w`|_+!$|8l=(!JPMmKx5S{CtbiV0r^jx$_mFeibV* z+3}11SX#iv;XZw&X z`L{of;_5yI|3Ie`~!CA6+N>8Vxg(c_}|L$C9r~%2RAM-S5ac)UJkG_+B!m@VG|52cgtgR<)#6dxvhdKM?=tGqlujaxij@xMpN z)~%52Tj{r4^9CbKS28`egr!PxBgl4sjIWW1$t~7oLYehQ+C17cOW87l?@W&)WG8Ce zX_)f>Ifx(%T=Zi4^S_&e=0J{X63PA&MA~^#t@W-2KSrTt$iw{~$%on4qq6R~|rfMfzR_Esfe(BbqAv;>u=mfGW?`Y1Q zeG0O6Xs-!$b5+A6V!S;%baBJgZ1sKKS_^6RR?YnLXKgLVOS~7@xULkwfA?wD&7mhE zKa8^r2(F+Qcpb6*O+H1HI&x^brg*^CSr!|u3FbG4zQlG9BdkVOPi8*ntgUA;edO55 znGH}^wlP^)M&?Zls?CnlM>iQ%*yxBkw_}tT>>QgOfLCVOIG0HA4gjb3_jCvww|8{LY)Hj{oB3elrHLz zFp=k~tSnk|&hIS#po66yHOW3E2uY!VVmV+CNw z!w+&ly79(x)`BS(^;$ht)S{2e?-hU;`VlT`&opWPITQ8snAiAW3tFpHgQwZ(gINSx zcYiM=kcU5858UK-N+&dkl2LpIjY;eJZ`tFo6}$9%%D|+eq#QSpap9+5E&9iN+w3pC z$(x6{7~gR3k=n~|2xe6}GbMS|Abf2)OHWRo?H9ZEAvjs`6pR>Q8*K%0psG}}>nQpE zVeh@;sqX*(@vBmjP$XoXbkH;svW_I7tOhFMgd#h8b4B*1Y)(ctheBj!CwtGx-s70( zn7`-iNbBmluJ`-<`F=lt{r)L8$$5?E^YOet?vMN9{zz$=sX#8?R~2+W0wFX_ZIJtP z93KYI^?Vu0`g6e(f~}L-39;0Sc<;a%H9|;8iWi%mFY#gX*b4!Ilsm z|LHJxKz8zrcVamehQ6E_x&=8DgGQY!wFrDa$whsu^#4Gp2gE$@b1+Gu zQ7;L9(pUa@&4jp-mi)E(BcNZjQvXnGIYobn27yr20}+reYMz$;M9n-uK0|~(i(xJnpdh^C_iM( zPQ^&Vq0x3T6__mLScXo<7ga(;$HL?6&CWNk6Oe;T(fT38O?<26+{7!85j(&+;EBAs z^w}6^(@c}1K-PO{fV@pGt*JM>%!Hu&3LAszgOMlI#NEBu{Fdyxl|>1C8o>W7 zWCaF*0Dz2)H0Qg3R|Wvx*-#GF?q0=&u*sXJyTSXEw^yq@a_SQ}z(MOe$P3Fr@A{5t z?B>}&emnXEV6`7+7l14g0};@8(i4_rN8T6lb{FoDnQ6&}mDqWG`+k z<1VN39LiaM=kUDqZ8i){8+HEHXZC3)OU@+x?Ief4tLOuO4wOu^YJKJS`hLgWcD!;0 z40Mq&{-O#bWMO(&1Pj>L%efO0i-3jtMIhgf+quWpf1hAb^m3&D!+#M;S<^RXUgLCh z*a9uUys3&AJCxwLEn=B8zdEo!D+xRtmhDP;53Q$x9>jC zQ&ywgW878Nl^dy3O{>bMCg@h*SDE!|)Y|klSXP>t)pEw#<2vtQC)QF*H$t>>?>mfS z5|+)15^8qGBOnJ}$NSk21#6Fsyjk?NOT(y3@iSEZsqt7qCyP6S`1+`GnxesBgj2be zAB@}ffaKKH2$zKI0itxG`j-=mdf+=pBdMb%k)mgmjutHxd37T*&x}!3rb?kFvGQui z&=)nN2A}05MWL!Wz~qU|M)qxm!#$m>IzEB=9(Yfm!)A%{=o~&PIVBWO?WKtGh}@R# zb)9SzkN{~5(J(#O(ifG0fDJBT8eOt~kM1a3cl_2vQK=h#y;y9lS`{t2DegW+|J_T^ z&!V0zpgXhJpVP1clUq{q(Y@)S_%G&Q+Mj(b9l)kuC_$}S6mBb2^C!Su%Hymn-%-Se zr>((*>~ZhZJ1Y^l7QwtN3^anoBbqdOvWjGdijTeQ8ocP<^d4qnW^+h@1i*LTsw~!8%~oM|C(tKY5H_-Y%YcC45pl z#acZ(ZL$zJH}x^5EG0FZ1@~_PIoOfu=mLXfMPEH&yPiwdmID0}DHsKuMM4UE}q_ms>7&17!bf{)3jaIqWFlb1cexnNk4;cXW)y6OLy z2Mx_s`F2B~Z~ibjO2iSq*1AuajBxqb60 z8`^_8@CR!_UOe!QH+yeICuA?EERZKO%}3s_^u^LtFLL*R-*2XP1|0<{GixVIJG zs9OUJLj%ucxom+w96;@vE0eo2ZBjPY9wGlc{;=#a%?X01B#zx$elHepw%K_bofwV^ zc`LoVA;oWJK&Mo%uw34}yUvQlbIwFTJ_DSs(F%p#qr)Qlb_c-hB!(_9A;O3;SrNZN zSo|#fK>7{+@%5FCK?|=``HIiiZ-ak($ku4JXG*e9z}nvgtmvt`%c1I>hRFa@xwqvc zuR@lY4ynxvh!l?2=z6?oW?~xTpEKz%?bk}N8g0!D*35IFM_{qwmx%Ho#%h&f5zvdS z-9PLG{SCeByZ(m?EFS82Nw@QfVZg_<%22Pod@C=Mxl~h3S;n*C9=7_|dy@X=&+-5E zITUb29Cx{4PZ)j$KN@A9Hj=J;FeW`Im%?CdJ;&^_CZhk3zs$?1W`_aw%YvXUC^GF0aD7g<+K6*Pj0sSRLbL;cn8 zis2<4LK+R2LQf*Ai+*BDQD^*Gs}`jTe=8qk)%+@l1(d;;Ke9+ny06O>LQlSw;37VL zxDy5R?rn#bgG!^#&ibpaCDf;YM-oN#ZoUo&i>wt+Vv3lI11`&;_fE=><3zfwIf0a4 z#I#$zqcF5$LNs?m1gA)VQX=%rW5}p?zjIsc>}gyhSV{YJZw=lN>PU*@_^j8)`j1Pg z5SST%3qv3s4_7T7YzX4#a{D)3g^uMi<^$ajRIBO7?ll0Z?(^FC2XvdNN3W08&RFjI6HEY}0P{@3kYVh8z6`<)>5;t*D|CFl z9*kADPmC=;F&y4I$%V+^=%pXxIAJmc`sYhKSQtqg82Je9%H}D&B<4^-@Sm71R6kWd zSZK>>Z@^wiQ>DZ$F+Q$PpTd)%v*SP9)rT{$@Ba@lqWBVMLqSa9I3P4t|(eiy6h zAcaHJiJ`T(B#N#Wt3{873&G*733**9_AP+|WjTfDticaPpR^n?mox(XsTzct^@_&d zWLVp(ajJfJ@4bimxDO-u4V{~Ilp@VMJp9)pZP-a_gKE%+&WTeUv$NaIODHXDVSks} zmZQX$m0G&tR=6HOGEnR6>2Z1E3r?#vC@GlWsMwl8ho;FMY*Pr~J9Pe~h_{w2U}%8R zWQffR-YVQ`K}*naBMD*I0yFzjuChj4@Ual$`A~AWzJ@{-9hc6FVJ>=i2M69hUiSN2 zB{5btjH}OkEQD1)N~36cVhT%HmrP>&!`TZ?ry`5qe0-bc5O(ODP=3miXNRio-ZIRg zt$1S1xpO@FLsDSW+7!%Ts@K;wMcCH*sH^_EDoMEc%o7JBy`JZv@=F4&E~75lRL^x4 z&}U#h$R65F85audtn~h(I^Xg)Hyk$k#$07l82=b;SwQd zHB@>psiE4FNyJ!On!w%1+o;Vbe$F{WhJeWK4VVrb(O|{NYv1b(o2W=ohxxW$@r%=t zFuA6o%-nx#pGb_xsMc!@QGrAKH!K)jHsI*TJ;vJJ7#l|{ zPXRaa$b}(TBtFrDnD>dE9Q=VR1s`rgFeV$RMfkFXFON}}ujejO%D|gyO_q~SNS{(R zHRmLY2dFowE+xr!ui}S+6q^cqBnABK5f^X-WS(|Mz4ueyXj9ZP<*c+K;9kxeQIDVo zDAJd6(0I%?u3r;KH_Nc0kiuM6sPv3W<-e$a!Gj9#2AshCU9?n zona+Gz^9x{H2Afjqa$=1XSpOa1K#IhZ~4N^1DkvT0?s8B{lTp|&IQh`EP27q?iKV) z2ySh>m`VXig=+no%?c@`kXryf!BURb3}ZGwA)Me~>c%;qEf48gKLu5-R6gNM7Y1iy zok?IR(Y_BDf&%1}POK_=2D+9{_nk1+b!S>*c|@ASke63S2gM6VAIfNY<>Sr^XV#-= z*QM>60ZP;9DOPYS2kc0KGEyHH8FiBNzQyBS2Iyehpri$y{5!d2MLo?q@GdY2H$4bw z&ra3SQ^1*{C%GgR;I5-Li!Ux5W-hc?8f4W9pY$X>!CpHB4yqOfOu0@t5&8A9f%rpRj9q z)Rw-8F-*qR@+Mj~i?foi3;g#01HOQoNY$l@?9Z}DuFyVBaPhEMN1uaTH(Xb-z3R|ra)Z_3085`lg0M+xPca}5Li>E-1SJr7sZMYtSMq!cN~{T z(yR!sNTc_>ZSfhF1mLrwz42bT`s^ELHE(KPGqx@xEHFkpk)b}Af}#5yawtOLDh zijP0OD5KpM1^o@;M2l_4DF312LwpECA7+56bLphes$9gRYo_#=IrhdTnPZbxG!g~g z4O#JdI+LAubHZkPEpva=SF2(Wl2TOlP5uR-nLexiD@ga#`@ny((Bew4O=p7Y*s+QP z>#WSfIw@daUbrk6z_xntwXB_jTB2wqe!8rj(myaaX??y?pLOW;K~dN6R-ug_6VnIDIH8KH{fB=~+gZ=$+;eX9xAjjqJ><}QiP`Ak7w_Irxo$JiBb2@O{G9&5ROyjV&CBI-GJcXuA)JYq!=5C}}D z-cQ=!{&bNpmrcD#V~{^TSh_Wz&n#j*9;q>d zW!p-r%rNNkm@gc+P@T4MSXynP?Eo*4_KgrXPZ`#P*{2D#Kx1XHb@GAck? zn<{l$ETxeLcdrb-eTM7QzFDe5h3uMMzC*SlNY{EfL(J||u7~EbxJHlE;v3Kqg{V6RN-eTs4erBKHJt{ zmkQLtn26F9VX(0SnY^m|k_#Ix@^hvs_%ZInvff6UT`nnQmK!?Gqx9YP?g}RsOjJjz z_4Fw|09wBht}|<~3=VfcdXAyUd61^}?+F!UP_sWQh%(mL=i*G=dOGRCvb=yqccrLn zB452r15;&l(a1td>8GasV#t1n*`%j)OEqZ<*)u`J7g9Kc)#r*HiC#7th+!fXw%iKM zcs$80a?QV}C%a4e5Bn?^zyx~Vsp^d_;Hxhli_FP#6J4_!Ok3na(YTl!pU!^Fb_Ml+ zEq}4+DOPXcsW%0kK%@)r>NhQO*UA#8j5>4X+A_2|d{$Q5N^%a~6Du}Re`0XYMkH5r zIc)En&FsRaxks0u&Gp^=(z79PK4qx`WnzzAqG3D}#V1WH>_yCdL$s>#|-=v-JwU1e!j?foo9A?vIUu_~A)5rH|_nW4aXdM~Bqj(cZW@X5=@ zdE=a9)^F^l$%ft?LCEs-`>87B=CdF8CqxSZu;TInnLHA>);Is)c0f8~-rLZuxaT_lIou~)_o z-755M zq5vt0hO$}2gw4r#3NWW2RlAIT%tW=;o6+K=gf%9K`Xg{#`Xu5F(vT;;*!$7(s`!}( zX*6Koi+(-p(!9-0LdUqk3zZ2bBwGp_dgSnK_F>baFP;1ocN~X%{9zN%=PD8!V4 z+`>V~;qJf5plyE+(FPpPJD}hIAvk@}{|do{ra?K$D8V$81+c~mNaS9AREKltUOH|2 zA+`_XGEB$Z8tA#FVG~PMm}ha5CR`%TYrP>YZX@WGExz8RCBt=lx8{at02>+?&65U9 zDz_BSUb`MOkzH5dlpY=ds7*0Kkkm-+rV?gh39UgqiSnp5xD02j*&EPs@lW*e^Dp%9 z8P6wdv&lv3E!voN?aT@A4bDnB+udYFs_I0C2%xz8L>#ZFdScmdcpD*GjseFPDN?mW)tOoO%TGZVdl&avSG?lY0=E39;7z*fTayiat<6 z;)eM@Av3j)nz*?G&!T|)g|Bs?k9%5MAw75*y*7wV|_8`S=s_zFNIZht`} z|F0Qb&_e~PEQ~>Xvc~iy=uT5su_NayrN>^#K=llku|SY|XLc)yt^ALQ3_FA?IDf5u zC7==mc_36{kV*zVPp*__dhc`!gbk4_Ajn)zGuveV~?uduq z?tn7s)vzeVDnnH`1C6|NgtjIif{Q0?!{t)1pr9U zcG!jD1SZewi6h%bK8(z=e+KuTysccE6qS?Qk!iYc|M5-w896ReTSh)98T3%~uldpg zLXib_*Hzc;)e`ZtEe9)BI)^z+EMr3?4zqAjz=Nq!i~bVhwi8YIYI9BR zJc?VY?+pdB9AH&Z->UlxPo94(ZjHS#ErQ_W^HtLCLougKA>3UkiR-G z&xqI#%}^J~6Cf~jqo8Cxg-8FyxoW#*rUKL9r;9GHc>`pO!;o6G57lx7$i-rfESfWi z=A6;e2)Za?4wMTa@;UoTU=slGvzgoQdZu#cy zRt5RK+$gs5R>|wHLyVrRV8c2RNFWZL;ml2!)i51MN!*A~v;VNPk;hYa| zKajru^tGDd<4jG@IOuY{Mb^|kFx~1F3INY8Dzb`Su z=F|5q$09AlD315_DvA5LY|mJbtsf=_-E$v_cjEC2Vo3`4$c&+??UQW`Gi_-oF%_jr zIa!ngi(aU8lAd0{Wa%AR{tG;I61vM}PP^HLAN2>@PlQ&HTKU!dtEGgPBnJKS(5`?6 z?D2~bX29-CLdO0zxc}~NY@B+ko`aVw}n;omV)jFZoT(egLH+~OW4CLP>7C z&}n$igs@tOz4(az#-K@^?qNn2;l)9xmsD+v)!Spm`Ik0&m}@-fI7M!g!?&@YOtHdI zRt0V`s-oH(e>CEV9TGweQN?Vw#%&YMTZ%T@bOzRI@0@!*FQ9u`S(p0xSBC~B$xy0G z#rpjz13mfXm(Yu6ZQD9EvA!)8{ahjo<hg()hjxrx=wj5o@OJg{GG@KqjP zplY|hWNW`1>Rd0pD8Pl*8kX@ow&=qt!xlsaUG_EJ$3v|)5Vqm^DuiMv7+GBT^ z!+&Ib4mlD~td*Bv3rF=KL`%H&4d2V6O@woQ!wfoEs7V8|I3Q9ZJ?^*2QMCiHM)URlvM`m-1 zQ|?8w(N)*dqEE%26iZN4L;-a~B5%+}5>tnJRBd@U7;lnlZ{jQA+@y5AKh~JY915Ad z%8{p;L~fTqyGMPf=t9u++r9f7hh4!hV2u)K_7a&y8A&hCfBv|?HL@P2O{+A?I#wKw zWN3ipy&F6na07LMtZ3D1d!(8Mjp4&=AhU51iMU=-ARPzoh>M^8XJxDO7ZK>yMV`f?{H@EbHiqzj; zu&;=xA@tsJp!BjFnTm74W%~-vx3(1r2&&mZ^ExGiLH8lMnCse0o|iY`h((%5i?i5} zTnM5rW1PI&$hc!?Tg3e4Sk$3anc~{`cAGd~g_||J@XCZHgz%P~MKC>_zUn5b+wg_B zE;h73$B1P}yJ(Jdtqx-?FqieZscwA_LA8oMta?qa9G%FIy;dwde#I{fhh8A+KV3b> zL%zLbP++L~X2VHrF-Ulp=-r(Cd%ti5SB{vCv+Ovg#*!CldFx)W?c-@{EpcZh`x=9XE0zAY5w?y2Q_c#80RH* z!x-lB-Ul5JEh?#h$8L{KL8C0lAGA{})uHz6!##4VF|u~eMM!)H&g-FDQZEJnprZb`VtekD)mmRYQ=YzDGg~R_KrfVFwZ>i|)KM z)c$ZCsA@vRK^;c*2F4o5WJb7$xFjAJ&C`IwP+V>k&+OaCg8`$PuM4=|sap9O^jd_i z&K@Y$^2=(!2!6n}afJq+7J2Z!f{RxG!YhFq5!Vo;TIGycZZU)k2EZNnYrz2Hl+LL1 z<-Y0RUsO*KIIp_x=te|~@+(!&7llr7%7XE(S$))G+MjP3Nu*Sgb3SScId?#SbvFF9=mSPK z*R1$OA0J|kwoLhIP?K4MicS!z$R%Mq3+{GA)W}*Q1@JAKTQOEI0@n+*l2DKkgM@g_ zX{W%j?QjYzz0%12TvLj2=*LphA~J5Fc~AEJndQw(I}S^LUO#|(;6o5`ZLV3P_7yHB zhjr_@Q_T=Dq{{`DS2uEN(49+zcR2l^LYL^>1fT}v-30a=-T{3`TM6AfCjt!Ea*us% z3qfQywtCURMaVrT<(Fmb-Enfr)sp0_uGji7TH^lwH%krHbtyE*rp|a`ni#`&qBC(bN9^MreN;B`_g>OiX{s zuv4BoOLXq5aH$AtnNf4RHb#n9I4M&U_yPf=l_4ZTBIwvZdP9{KOY7_aaE9%JjiX5G zrWQi|dpjjk;nvAJobqpe!c5=mRXBEvU{~gZs1vplDP-EqC(}llT0tF_j0~)N#AO`l zO>#1~^(ipg3BVrA<=-N3r`)pNfg{U_x3EXTCJP}+VI1FWxYQ7HU9d94Em@^yNyhF2 z1?s?WgUW6)aAgxeZ;qYbS3Mc**dHWoKN9J1NN2Xi9t)_GTR=Lu{7@HqpNo5H2_#Ke z9Rb!1n=kEt6Ut6QGjkfwR}^MHR#CJn?tuK9DKu@j z{=`1QTkR!X?co5b>nEcz@tl?VJ|l`(m$snHiL}Cdy0I{3Q|eRqJJDQ)TyR zpT$=By6UpNa}^RNeb=WuiQ<%4p7+gh+43Sgf7qhxve)(E33TU?JBQCK2QKzEvICPs z6T=F0=iRmEhA-FuYc0N#j(C}TBFZie2=(MLcjdAct>&0tyS@!ly8?J<8L3@q)f%o?lqCvhe}6ue z0ZAP_=c;7CeGOZY(sE@?zovcRSsfPPJEGPKshQ`7+A}zOy8Ks(td#y7$6(fz`l@S+I-AarQImykQyb z>knrcIy|@PsVrP=RyLwd!@Z(|twcD(*^8JWEV`SS7-y|;&QCi$8?qe~CQ*h2*^}pn zWe6n0^^(Bo!iWa_OLz@P^JDs!UpT zJI5{}Nj6iTRx3spP)053^fPnkN~lC8n>!+?!)PzMW|>?2R$LySWuAE#U0gJ%Wgial z=XOCtSrzOcd{415o~`m*lhPmRpF@4Xy!DR3o_0{H6E7Q6MBTrnJ_9Prs{7+h4Y5rJui<}H|M_E=Yn}e5_x?5 zmZ@4G7LNBWKwf($pVm? z02I&5UvG^eAI*rwe}_drqCFXCI?Qm&n!rEVVQQ<-RU(cz^1b0l3b<>zvNy zt=Kr!8FTC2ur872I-W5KYi!}`GSUE<96HwZs(z1#8Q3i;08zLY8XXeU33t#X5Q&ef zymuOYpX)?m)IvSuXc_G>>6v74|R?mT;T?53% zo^llM$8>&FKzc+7Vmj6%*}`vrpp`&!N{xO>5)rb>H04wTWR~6czSdJiQM7YrjmsLu zf}TG3VQ1e($Ug~?_Cul>dwL+IKL}JlETEm|VQzR($34`W23WO2MJ3P&5p_a8qd5!H zRyDCMoKQ)}2y4|%y=fW+AaL;oUM^knxGC4A84?P?pRj%Q&0B#TH4Z-V_}ln59J1TIt(QUh@j zk?%@@m{k%1h#FaT!2&tCL&brPgXn0GjL^WDML;z>%lO*jYb0M}V5mhQwz@0c8mk_? z4S+xJZ|W6rYvY-82O_O3_M9Zd@_pxMQnPw!;`ZOGrvVy7clud^`39f%r-IdF?Y02E zbUg>;LIFnp2}>u_5OLWacN<;T3@&!xvg|*JXfuiQ;CF?xGsLb~o3H+#Sjt}nD7k-J z0`1ffrTwxP@ahw7L`En~d-fUtq-}7*_Mkf3>W@#v2kcz2caeXdU4-=&?r=aT=*-R^s#j5Uw}bRmc-Rcyv4RiV>EjjPV7i3nE9_fR?Yt+)i>|pI-T>3C zyMu4UI2J56bm9gc5`#B!Ljs0;acNv!VK5qr7r+gh3^{Au+^rRD>M}$98p0!MI-rH2 z!v0GXKwbh8A%dWdqT^E$w4${)ZQN^>Oy zU)tt^9LL|wQ}8u@ZNq6Ze1;28t^SF8?VA3Hr;X|rplahnBBF9Uz^+d2^UM()wfR+i z|2F^@PkItx*@1M*aZs98s?33hb^}}z$&#e07s7D?{y-^L&;5&1&V} z(fBbE&6v~iH+X)Q8-N!K7QO@%3VOos7Gk}5HM*&?UEuNVzO&hLJaxJ1YrWk<)~;AYTT_ zPc@{$AR;exUpcpaH~`GYhMIOHeP9D-m0^n%po1otI-w7Qv<})qkXS+eJiFRG@b#GN zs2h;g8S}HQ4siiZ9Vg3iNKjeWEy8lNl={klRU5@C>S@_dfaBMA4~Q)wPXHQ1+*2T3 zWTK(6x`Ed&O7J|SueK4N-BWynOy#A1GW!)!?fh14lrNBp?{}7VFnFv&)rCk8yP-TEvzdZj1-T8t{5j>f12f)im``Y~rEQT58Q{nWyPa>;O1G`HCPQ z0l2H5Wk2RZUId3ta~=ymI6*bio#4cApaPUg+1FY@;RN4njpS#jxLf^GPlzKFFhKv4 zFWI}lN-?JDA}>L!-sE%GWI-&?8hbb}488wH{y#Q&T> z(IK}6@h)Ja-@dvRcYFEiXHN$6(@xl0aOY5As|v~}yU&6m-Fn9jsOQW6({dYVN%7qL zAcSu{;jsF8$~S#G<)M!~3UzLq*sGHW9ikd)1sAknEozIuq7t(b-P6$F0)A{@qnfVC zc0y5RY}nowXrF~D%dc+l&@u3<)cby~y8Jsy8J_Pc6N?3%mTV8f88kIe0@b_I!41QQ zm}DpL0jv5z>M5(ak3wG*C!$l{NJDx3RpH%*z0;2M7i{wrjrZ$L-rKm7ms3sb?&KGF ze_De6Go1j?aim%F=uuW|oQd>KaQY_2P&@qZ`f@zH193IrD>*z-weN@4+nq^jSt&98yZklc*^8C!NTjwf%jBG({cy!`v}s8`26=`Zm8=o z;FN@AWaj8b`3dV|Y?Vo?fL_}{aG)V@%CSQZzfe#jrMN)87+f$sgevfefE{*v4ya#t#<^*0eLKz?;iAu4kW-VxO29VqnU+D9<`G9T~ReT(d zXISiS?=mbqs!kz=U-}CEK{{Hx{}n{T)b3|%-l>0vz|NTL!Tb#0B*((==nYT4;I~Q{ z^)cYOQ#gJHl{MFaxBj27{IzKHLs){s!Z*@L?A^{M?!=13Tg|w={OZPdd3F?@Ey6#& zrv<-%dOzg{zcvp359ys>SN^Wpo2q+vYc*|4ySG?71-3ZzY zDBy6#%UZ@N-X}T^-{_8C8#QbDK#{UmDmnC;@t@?PL{#{;Hx8=rM?4gpMHt&NqvDFF z_yBCllaWV#mnM|iCg2+cBPhD!LBrO6)}2Sr_Q%e0dFpR~kNqV(F+v9@BmVRWiGZY8 z8CuZ|JE@FU;644=C{VWI5K7OSc#oq6(m^4CtaKsT1Ra4f7 zDl5hU!Y&{usmMx&d%irm=xMX%MAbj(q)VMce_iYl#BFpnOJz-(6RQUsIQ>qWmWLW% zUIOqlNBsBv1>VJe$K%D>6QK!LgSU$CzvDIU!7uMg~Zrk_7oJDKzHO2{6FiC z1YW;Dvm6o<@?&zuO$DqZKuPMtaxBF|CwQgf&3dr6PUUcKVe z(;jCksukD%3q%2;b9A~@78oSAi&>4r^Et8NMfLvm*kbf-C&NmS-6i4rO6k|%na9QQ z5$CO+na8XjI}QVa2EMCxueKv_?;h4a$hy1+YQeRkv_9gSU+KsBbJ(1FZyFBOb{Q22 zE5Ac{RnT3w-muIyH@4|C8GfoOnB?HY)mFyaePZof`{5?T51jrd|F|4J6y@4TZ0tZE za9^|cTSt1Kz~I(Oi~{aC>FZKi4f@q(h6N73qEK>Vhp6auVcC3zY^Y%|8z(7Dex?urri3*`8 zqMZv5KEM4ix@)0IXkg{DY;YRHVXhHe#1L)vCgy)5F*Sc8F{gz~`b|rK4S=!yz!W1g zl=K~9(emUwTy5uZY9HPbJ$?rj>~M0+DNt@rDRdy^7251sF=D}vuFuA7x92gEDLP=6 zm`Hn83QzFNbT@h2r|KE>mVY=}?z{MxuE&QmXXo|p8+`&Qqoa$rA{NUM8n6<3LVLHZ za|bMnwQANnya_C*@u@3Yw626!KmURhZkNBUH#n98@ww<{nlZJdF$w7 z)VN}>);jhIU$zwmP!YJ&pN(2rS~3j@3p=FSkxo(*bm83e#>M=j93ntij5t$h1+_bK z_~S8WDeYnPR&>Fxx>&d|+Vp*OG<_~!TwHa-5uPn=R~qH4AqQL^l!0U;0Sw^6EHzcL zb|(Y3OsHY$c-GzJ@SliN_umnxs>g@|1fIk4_{ibKdPm@VN?7%iEHfPf5}!Ju z?#Owr60J*awt#vKSS%A9e~p$Qa9=|&xSJmv72t$jG^jfQUkZIY6X)8m#D4@HsmCX? z;#*9KIwf%oD)M;|F7+qDB=~W6dt{7vIwd3LZHwtSF+&BMqJE~PE86)2Pf(1gl#Z}7nHg=sWDJHm1^w}v!8)%NTGNQHdT*D@S- zX0e`t$Xo`9-ngIfl@ds20}Z;t8)Gayeiro#)nb0 z#5q%yqI~Pff>q=UtjYyG0_Pt}TGgd;Md`yPgWG#k81BiyspNrp1$p7t2Fy4C)MUKx z1KvA93^P`V?(Frqt$rDs&9jltSy!-!q;(kxY`KF$Y=8-n1x6<>nu-H8}$u{!L)st@d* z3J@m8-Mec9o6j9EIocr~W>NSNkTsAbWuP~BH&rfMm;arP=O;#JsvopoFO4_Nw<{S^ zQjoKbYs?i;Y?Y;pd!le@U!N-!>MSm?!(!*JyLCelvd$k|x%t*2ts0|4dj7|#mAvcv z0@VrSDF31jC&okAW%}Vr(Al|N%dYY9{m})@{X&P(hCH?Hqyb}tvO2}s(WB-)IZxX; zEdn`3ktZ6;9#~I48K@zsu0`P-1|?+&%u+RI)AQ+w$Em+Fh_FZW4NKqw%LLR1$+)# zK~4H4u2~HDHYGqRvtqUE#XTx&kyBds+l%Sd^B<>ddPeW7WG$U#M~t*S?O;}XPUam{ z4tjEnWgUmb*#&9021cmDaDO&1I(zD}rRI-RNWiP@FB#pxXVcwW+6O%^U|eQ(0jFiZ zC>$v8QF&ju!D15!b>zMsWd$Um3&7XQBkJ4!gLGcW=~&d$=@Spvq_&YA0qV4L%R?kRU-}qgCaSzi2wKmP-+3 zGKZwt*AE<=?bkkiC21fL&JJrttvF82t3MYnYe%XBB=umlZe#4zS~~RL84#GS%1?O` z1w1GdzBgt@4i_gYIBs7&H}kZqo4c-LjLjO-d2{lEO{{j_9>}YW=3t@y*fY8ihZi8g zhOZ^xJ0E7S1vIC@XfT0iKa~VlbR;GdbG5L(@X12MMiA#s2Oy#XQ8Sz^1T<}?pG|+| zVEdRKc#{q}r*<4cpSxt*Jmut#&oZd$h+j7O6t`^LuGOmv>;GJd^0@%AFEFHZP6&_v z`Meo_&+h#s>X?E{T)s6)`%xNiUV`;&UvPP^zEgh(#!3Yl%b&dWtGVQXZgEbLg`t}~ z&$+1B&K{2vcFU*Dkt&RP7d*<8D^0Wg@v{X~l(e?Qmh32Ep_I1#v$=mw?v5g5#Nnwv z)Wm$O*vFEI2e5;Q9aAJ7>dC8k>z7O7ru8z5&JECY-f%KAa2#eI5JoIaJ)Mu#s3Y7KumV9zS#U7p4lp_>jlm@l0QH<&4yiV1M1X=8(2mWx*ep0aSZtcd zwP_tsd6#Fj$qRKp?@`cS?(6du^wMYFA}op1y{}Si4v9#L4jTL@O0k^#cb;m8W>}hI z#9Gnbx+cV;e5_BosK$TRTje~uIs~8liNB52%-1d;pYoBk}$QeDDKGba11+!L3(e7Jj zf{q2-%%`8vLdp+z0vt6auFx6=MpNQDG90Ms$yj)bDl#GYAdwH(vU}#6Vc_=BEuvXW zN4&F&hvLX`G8O){lYRlo!bWyeMu^F<4|^o1lj3_kj1r#}7T=NGbxIxEMc>6XfzEWjHv6uv1 zbAh3bYQ*{ba}Aqo5F>|ujPK6^SIQ$&>XsXo27D$$(fX=Nv-emK!$uie{Ui)(0U2^E0QWNSL%lTDR)G8fndZ1h^9e=kY=M>aR6i9j;% zPap=cw#0WtCr3h0A5sDL)GC|PYs zS%8qtP4@r?VRGVz_tX`?+DHLjDJiKe?_<0oFN$A!Zha_mQX48;b@lf@n)-kD6*!ivQ)MMNjX|2ShkFNS;cTTpNu|)#`dp~t0jZ-4ayF--{cZecBKue9XA#az6IVVe;D1%iB>@n?DG&iY+RFg;rV( zSsml_YvmD5tTz_&PLlR}uh!iRa>#hn_m$b8GH%hT%z1*I2gJts>L@Y$p{ZcBmr6a6 z()J}v0~>S22`Y3UmcV6*(suT601vQq+8c`y>oeVYKVL1MApUWEV7HrOW=P7;L%-)T zg&nBl@7OjG6&<`CTr^tV*qgE9__S`4d%x|vu1#6IO5gxNNFZB|zR{r8GtLieK~oKU z!nSK(zQ-1Zw>rv5ir1xUwDxfgZN~qi$jA4gtW~={=(v#OCHl;bJ%1K|bVOTagL zl-w=NkA_&0{7q?|=J(=RJRtxbdkgmSC)3hgU4bOa^j3q-AxM)oE=sgjDdVfeB_Q#> zYt;5CS@Ap5PLby7prJ?GSS3szJ=_D*@l@_xo4X4dn}_d!^aa#I3vOBfhQ`7VW^h8o zw*7qTkl{2;EjlU(WMJ#r-dNL5RZLMNUX^bGqcZ271O8bS-!?cjMnqqQp7{*?Fo7}& zKiJUvm`~`Cyx?OBxLBf&9RL)%Si-4ZD5z@s>;%@UAG&B)&2SzhR1CT1)-N%X;rRhz z+|dy*M2SuB3mJ2psA5}343;unVE7ylZxy<8;|=(kvw|B>Ai4*n;~3yh^hp3TpqXhq z5+Gecfg2GK2Ai0Ra$c96EedQ|HEx*HOj*6GJ`wD24lnxv!#zmU0UC#fd;D6${n^%! zoSX|h4Y5ohye;#BuD6a5v|rY37vl8@k7(vlfYw*Icq+kARgpw+GtvGZy)ph$>;Mhj zkJ(@h(1G$WT~C!(`7W={_^rz*o>u*G_wLb0-9SptC%F7mjro@g+%-LX8m<4oT-kuq z@~g(qCM+FoiAHs=;`Ed84rt;8t5(jd!v!3YMS_q{h3{5&hs;+0QVRaxDu^lSN%{FH4@6X(aqB2^oz@PY4DXQN zC!0D+%<3I%ulJ8t!4Q15ul$8+SM&{*UG09mdsWr}Yrk=!C0UHbTi%+;0K)4(FIE8| z98~7{@V!_CEGUV8U*6=DQP1|a27-S68BhJ>|NQwgLAfW8$nRn;0GSxFHYsWU|6-v; z?*F`42xd`^)suWR^+Z9Yo>LW(fxj5ykZ6YepBv(Ig~ z=&B@lS}nh-`n72g%O4ubM7;MYwX!~%33MR-0c8$YwmRTZ<`LpzM?+8M_0$i4Bk->b zTpPO#Qpwn~XyKuK#ATm3Nn@MleoPMOfYIXO{b=Nl!KcOmVcvEHHm`6=lg{v^?6}<7 zjX3NN2A=~gYr9}mJACcD&1obko1{MAV0Zz`vmZM(Q0=tdmb^HoFhEgx?s`>eBqw7~ zl(CLKS--8icqi!dZHF2+EB~-QPQ#k{)ja3?!X6`R<&dvSRiCBz@wBiarLfz{^X@Kp zG?X231|~TWhN{z(oAW3OX%}(T{vLxggv|@8s#S+EJY~jq8u1Ql^X^$GYWCQ-`RckF zU7~CHRaL?57a6#in$k&(bA_cL)JaT*mf5G`&Q3f067ge$z@DP74Fb$Ax8n5kb6(5Z zlIP$ZcCOPEZTH?8=$i=cYBi{#&U#Q|z7d&K=L?#;t6iRsmWqkd+0SoqsnxXJ)ahFI zoUXr`GJXk=-BFOlbwnj<|MIYb$ryQe1&ee0LK|{!J(JJ zpnucUhOSZg#zH8So#v9ssg{GV2TVo;vI5k(PE+s;awG0H);&I0O#2w3`TQFx#~Y{2qaG<$oacG4v8H@DI_ zM$|Y3=;@of>X+D{`uGUkf|;OtpFd2EZk&+&MzX69^-C%-$6?Zw5(Yk?Lm8N$QZnQ` zaFbkL1|F<^8Y&0Ly=gVvvF3pQ8h_Ty#}xznplV#|C+CRm?*%#T|J8yVptt~r4T(d^ z9xXdmSb3W>X$Qpe0jIf}#T!bTdK#!i958?W#f}h=cCVW&N7eRYIdG1zR-nFYjsvf4HKq z)8Vt5GXZcRU{ zL2Zt~5^@AD0cl==6U0@?0kI^6O?D7G6FfnY3I4BB0xWl^pWKH#a%5oW3c*&n*to_0 zrsc!XgaB8e|Dml;saQIcXZ)ye(-wMyzu16t5nOrc*S&QQFoQ<*#rHa2+~w^C;xNs= zKl@^QUz$4-d3?`e2=RA`{Qt+^TZcutuWiFBB`w{dfJk?Dr;O6wNW;)IfPjE>mz2^S zf^>Ix_b@aPLrB9nxYl0#S^L|2AJ2On@1O5K>M;((+`s#}uRPE5dQAMjV(x1Klvl$Z zp-(W4no-EGvEw z`64E_s0w3561y9#8Sq**otgaC_Ts;G1OI@1?{xCd#7$lNZfV1GC^@<-R{zwYOg@4E z31QOxPOlbm|JO=M{|2=9pQ)nZ4%o%EQ#dfVq9tsL+@Jb8|L*+7{%SK`_3PE4z(AGd z1a3-)-M{`vvvG(%87yBF9)2+u^puzzwjAErtnQ#A4E3h-C=tH@n_J`$NaJNWac6@W zL&rtdsz>F~KgJuY!e1!NA$b#|<{@c*BKG|g6&~v+;=a_87Yb2!sDCsYmHwuOC^vXq zh~z_#VMRIznr z5q+Y_d|5!YC*b4T7>P|;<;Ap7l)IDhao`ai393ln9ZS#XK`sx(9X7-WRtD2{vrxcZ z*7H?MrZ`38Ir~c-5jJk0m$({W`z`SP6~6!cv7Jx8RIZ*bQj3zws?Wxce;9sMaTc!< z2D*G~{^K&N;xi6>SuO91ai|MoFiFoBe0B71i#ovuJ zj)EVWtl0%mu2)>G&*!t7cv~I>Sn6MaES$xl^|qdl=5sX{3F?ng6a^wG90qUzl1|CIi4)#>>< ztz82-%CYsiy*d1I(6)XYu$_sllc%pdmZw~VpAs_GG$Bs7baAaqa zRQ^LR1a>BOld5$G^Ll})>$wD(#I*keU~T;afVHc^Y75=T+4c%ee!E?cnyKpo`;~Y9 zAeEZfaYLRytkWj6x*Ya)PE!asf9|aDE{CaxxH@<7KqlL_)8;nOty!MWmguXr)wb5~ zT&JVawxu}}jfRg#xKCB;`u6nwk%P?9+)XwP~}*@vGQ+#l3>CM>d^csCE;;(sc1!M z|4$HvpcYMu{M@2dSZ2tEO)UW1*W?4r0{l#?D@PT}681_Ei(Z2L5T>O6Cx@>TFo{oc zeq+^tL13x5&sqJ6z;cgrdHF~SrCDEmdng(0Ty?PZ0F%C_-mao2aJ+|^#$GE-pz3|{ zY}d!WIxdOmG4qj{IC3BRUD0Ug@0+~yBKPW`$p66?g(<}R?o85&ogBzUJ9nJkxpu*@ zbUkQd6T3nk`7N{uVb|pvFhrM}B7Y4`DEdti0EVB0Dg1C{Onf)fxeS3v4!iyY1Hdel z+jddkfkFw@ceoNm<<7&sY0++m=V6*>*PE+_ix#TNK9YV&1^Ahfz;gQYhO;U{VBi zuUTQ^O3O?bt|s2eC4rnjeH12)`#*&ZNJIcTydVA-@PXg4*Xc2Ov5CO|1G*L-NOJYh z9|ytk7qZ^47h(V4YY&D^rAvcd^ck9>j$aDp&+f~!J&JDqDN-;r8e#J3--;CcErh~W ziDZhM?89;&|ItTu5DeEek_W>(-Ig_tQik-uct{$7HDw(>1MtXT{bTjEp*N42A4m6N z;y3j7#P2-e|0RBZsbc?MNcKg6Jxk_ zXQq&*aPYuHu||cRedlg|yt^``(XQ7evkXswO;;D$>NLt%3SY4a__Yx z#I4_|3kD!F!t>uZKQ9Qjs-WCAQw%%q

    tc5W?ZMhG1JOwkJQ^v808)&jvCxKZIU{ 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%0AxIK+L(j-Pny=kA5K~COdnd~_cZxd=V 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

    iLMG2RmF|D-WfJ&e6r7 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

    oJ)S$@dJ3>8O{?uEWgA8K@XQVv$Sg3 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 section for more information about the system dependencies +<> section for more information about the system dependencies for different operating systems. [float] -== Manually generate reports +[[reporting-required-privileges]] +== Roles and privileges -. Open {kib} in your web browser and log in. If you are running {kib} -locally, go to `http://localhost:5601`. To access {kib} and generate -reports, you need the `reporting_user` role, and an additional role with succifient <>, such as the `kibana_user` role. -For more information, see <>. +To generate a report, you must have the `reporting_user` role. You also need +the appropriate {kib} privileges to access the objects that you +want to report on and the {es} indices. See <> +for an example. -. Open the dashboard, visualization, or saved search you want to include -in the report. +[float] +[[manually-generate-reports]] +== Generate a report manually + +. Open the dashboard, visualization, Canvas workpad, or saved search that you want to include in the report. + +. In the {kib} toolbar, click *Share*. If you are working in Canvas, +click the share icon image:user/reporting/images/canvas-share-button.png["Canvas Share button"]. -. Click *Share* in the {kib} toolbar: +. Select the option appropriate for your object. You can export: + -[role="screenshot"] -image:user/reporting/images/share-button.png["Reporting Button",link="share-button.png"] +** A dashboard or visualization as either a PNG or PDF document +** A Canvas workpad as a PDF document +** A saved search as a CSV document -. Depending on the {kib} application, choose the appropriate options: +. Generate the report. ++ +A notification appears when the report is complete. -. If you're on Discover, select *CSV Reports*, then click *Generate CSV*. +[float] +[[optimize-pdf]] +== Optimize PDF for print—dashboard only + +By default, {kib} creates a PDF +using the existing layout and size of the dashboard. To create a +printer-friendly PDF with multiple A4 portrait pages and two visualizations +per page, turn on *Optimize for printing*. -. If you're on Visualize or Dashboard: +[role="screenshot"] +image::user/reporting/images/preserve-layout-switch.png["Share"] -.. Select *PDF Reports* -.. Dashboard only: Choose to enable *Optimize for printing* layout mode. For an explanation of the different layout modes, see <>. +[float] +[[manage-report-history]] +== View and manage report history -.. Click *Generate PDF*. +For a list of your reports, go to *Management > Reporting*. +From this view, you can monitor the generation of a report and +download reports that you previously generated. [float] -== Automatically generate reports +[[automatically-generate-reports]] +== Automatically generate a report -If you want to automatically generate reports from a script or with -{watcher}, see <> +To automatically generate a report from a script or with +{watcher}, see <>. -- include::automating-report-generation.asciidoc[] -include::pdf-layout-modes.asciidoc[] include::configuring-reporting.asciidoc[] include::chromium-sandbox.asciidoc[] include::reporting-troubleshooting.asciidoc[] diff --git a/docs/user/reporting/pdf-layout-modes.asciidoc b/docs/user/reporting/pdf-layout-modes.asciidoc deleted file mode 100644 index 7d747ad11c19e..0000000000000 --- a/docs/user/reporting/pdf-layout-modes.asciidoc +++ /dev/null @@ -1,31 +0,0 @@ -[[pdf-layout-modes]] -== PDF layout modes - -When you create a PDF report of a dashboard, you can use the *Optimize PDF for printing* or *Preserve existing layout in PDF* modes. - --- -[role="screenshot"] -image:user/reporting/images/preserve-layout-switch.png["PDF Reporting",link="preserve-layout-switch.png"] --- - -[float] -[[optimize-pdf-for-printing]] -=== Optimize PDF for printing -Create a print friendly PDF with multiple A4 portrait pages and two visualizations per page. - --- -[role="screenshot"] -image:user/reporting/images/print-layout.png["optimize-pdf-for-printing",link="print-layout.png"] --- - -[float] -[[preserve-existing-layout-in-pdf]] -=== Preserve existing layout in PDF -Create a PDF with the existing layout and size of the Visualization or Dashboard. - --- -[role="screenshot"] -image:user/reporting/images/preserve-layout.png["Preserve existing layout in PDF",link="preserve-layout.png"] --- - -When you create a PNG or a PDF report of a visualization, the *Optimize for printing* option is used. diff --git a/docs/user/reporting/watch-example.asciidoc b/docs/user/reporting/watch-example.asciidoc index 4f5f011d41074..4c769c85975c4 100644 --- a/docs/user/reporting/watch-example.asciidoc +++ b/docs/user/reporting/watch-example.asciidoc @@ -26,8 +26,8 @@ PUT _watcher/watch/error_report "error_report.pdf" : { "reporting" : { "url": "http://0.0.0.0:5601/api/reporting/generate/printablePdf?jobParams=...", <2> - "retries":6, <3> - "interval":"1s", <4> + "retries":40, <3> + "interval":"15s", <4> "auth":{ <5> "basic":{ "username":"elastic", diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 1d7a3f4978ee0..fb40dc17c0abd 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -13,13 +13,14 @@ to trust the {kib} server's certificate. For more information, see <>. [[reporting-app-users]] -To enable users to generate reports, assign them the built in `reporting_user` -and `kibana_user` roles: +To enable users to generate reports, assign them the built-in `reporting_user` +role. Users will also need the appropriate <> to access the objects +to report on and the {es} indices. * If you're using the `native` realm, you can assign roles through -**Management / Users** UI in Kibana or with the `user` API. For example, +**Management > Users** UI in Kibana or with the `user` API. For example, the following request creates a `reporter` user that has the -`reporting_user` role, and another role with sufficient <>, such as the `kibana_user` role: +`reporting_user` role and the `kibana_user` role: + [source, sh] --------------------------------------------------------------- From 642eb81014fa9a2002dfaa2529be951c6129b5d7 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 21 Nov 2019 16:19:51 -0500 Subject: [PATCH 002/128] [Monitoring] Refactor the enter setup mode button (#51103) * Refactor this button to react for more control * Reset this * await this * PR feedback * Fix tests * Use class * Fix tests --- .../__snapshots__/setup_mode.test.js.snap | 4 + .../public/components/renderers/setup_mode.js | 38 ++++---- .../__snapshots__/enter_button.test.tsx.snap | 17 ++++ .../components/setup_mode/_enter_button.scss | 6 ++ .../public/components/setup_mode/_index.scss | 1 + .../setup_mode/enter_button.test.tsx | 42 ++++++++ .../components/setup_mode/enter_button.tsx | 46 +++++++++ .../public/directives/main/index.html | 26 +++-- .../plugins/monitoring/public/index.scss | 1 + .../monitoring/public/lib/setup_mode.js | 68 ++++++------- .../monitoring/public/lib/setup_mode.test.js | 95 +++++++++---------- 11 files changed, 230 insertions(+), 114 deletions(-) create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap index 12b82be333703..b52bdb7f553a6 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap @@ -78,6 +78,8 @@ exports[`SetupModeRenderer should render the flyout open 1`] = ` @@ -173,6 +175,8 @@ exports[`SetupModeRenderer should render with setup mode enabled 1`] = ` diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js index a07a26f64acff..dadb31f2cc83b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js @@ -10,7 +10,7 @@ import { updateSetupModeData, disableElasticsearchInternalCollection, toggleSetupMode, - setSetupModeMenuItem + setSetupModeMenuItem, } from '../../lib/setup_mode'; import { Flyout } from '../metricbeat_migration/flyout'; import { @@ -20,7 +20,7 @@ import { EuiFlexItem, EuiTextColor, EuiIcon, - EuiSpacer + EuiSpacer, } from '@elastic/eui'; import { findNewUuid } from './lib/find_new_uuid'; import { i18n } from '@kbn/i18n'; @@ -33,11 +33,11 @@ export class SetupModeRenderer extends React.Component { instance: null, newProduct: null, isSettingUpNew: false, - } + }; componentWillMount() { const { scope, injector } = this.props; - initSetupModeState(scope, injector, (_oldData) => { + initSetupModeState(scope, injector, _oldData => { const newState = { renderState: true }; const { productName } = this.props; if (!productName) { @@ -95,10 +95,9 @@ export class SetupModeRenderer extends React.Component { const uuids = Object.values(data.byUuid); if (uuids.length && !isSettingUpNew) { product = uuids[0]; - } - else { + } else { product = { - isNetNewUser: true + isNetNewUser: true, }; } } @@ -123,7 +122,7 @@ export class SetupModeRenderer extends React.Component { return ( - + @@ -134,9 +133,7 @@ export class SetupModeRenderer extends React.Component { id="xpack.monitoring.setupMode.description" defaultMessage="You are in setup mode. The ({flagIcon}) icon indicates configuration options." values={{ - flagIcon: ( - - ) + flagIcon: , }} /> @@ -146,9 +143,16 @@ export class SetupModeRenderer extends React.Component { - toggleSetupMode(false)}> + toggleSetupMode(false)} + > {i18n.translate('xpack.monitoring.setupMode.exit', { - defaultMessage: `Exit setup mode` + defaultMessage: `Exit setup mode`, })} @@ -173,8 +177,7 @@ export class SetupModeRenderer extends React.Component { if (setupModeState.data) { if (productName) { data = setupModeState.data[productName]; - } - else { + } else { data = setupModeState.data; } } @@ -189,11 +192,12 @@ export class SetupModeRenderer extends React.Component { productName, updateSetupModeData, shortcutToFinishMigration: () => this.shortcutToFinishMigration(), - openFlyout: (instance, isSettingUpNew) => this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }), + openFlyout: (instance, isSettingUpNew) => + this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }), closeFlyout: () => this.setState({ isFlyoutOpen: false }), }, flyoutComponent: this.getFlyout(data, meta), - bottomBarComponent: this.getBottomBar(setupModeState) + bottomBarComponent: this.getBottomBar(setupModeState), }); } } diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap new file mode 100644 index 0000000000000..2eaa25803c81e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EnterButton should render properly 1`] = ` +

    +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss new file mode 100644 index 0000000000000..a5ab07618f267 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss @@ -0,0 +1,6 @@ +.monSetupModeEnterButton__buttonWrapper { + position: absolute; + top: $euiSize; + left: $euiSizeM; + z-index: 1; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss new file mode 100644 index 0000000000000..b9c218fc4f39c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss @@ -0,0 +1 @@ +@import 'enter_button'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx new file mode 100644 index 0000000000000..1a8f15ce5f938 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { SetupModeEnterButton } from './enter_button'; + +describe('EnterButton', () => { + it('should render properly', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should show a loading state', () => { + const component = shallow(); + + component.find('EuiButton').simulate('click'); + + expect(component.find('EuiButton').prop('isLoading')).toBe(true); + }); + + it('should call toggleSetupMode', () => { + const toggleSetupMode = jest.fn(); + const component = shallow( + + ); + + component.find('EuiButton').simulate('click'); + expect(toggleSetupMode).toHaveBeenCalledWith(true); + }); + + it('should not render if not enabled', () => { + const toggleSetupMode = jest.fn(); + const component = shallow( + + ); + expect(component.html()).toBe(null); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx new file mode 100644 index 0000000000000..8adcb635a6559 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface SetupModeEnterButtonProps { + enabled: boolean; + toggleSetupMode: (state: boolean) => void; +} + +export const SetupModeEnterButton: React.FC = ( + props: SetupModeEnterButtonProps +) => { + const [isLoading, setIsLoading] = React.useState(false); + + if (!props.enabled) { + return null; + } + + async function enterSetupMode() { + setIsLoading(true); + await props.toggleSetupMode(true); + setIsLoading(false); + } + + return ( +
    + + {i18n.translate('xpack.monitoring.setupMode.enter', { + defaultMessage: 'Enter setup mode', + })} + +
    + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/index.html b/x-pack/legacy/plugins/monitoring/public/directives/main/index.html index c989074b6de0a..bb34cf6f5bb63 100644 --- a/x-pack/legacy/plugins/monitoring/public/directives/main/index.html +++ b/x-pack/legacy/plugins/monitoring/public/directives/main/index.html @@ -1,7 +1,7 @@
    + ng-init="monitoringMain.dropdownLoadedHandler()" + >
    diff --git a/x-pack/legacy/plugins/monitoring/public/index.scss b/x-pack/legacy/plugins/monitoring/public/index.scss index 8200cfef0ff3f..41bca7774a8b8 100644 --- a/x-pack/legacy/plugins/monitoring/public/index.scss +++ b/x-pack/legacy/plugins/monitoring/public/index.scss @@ -20,3 +20,4 @@ @import 'components/table/index'; @import 'components/logstash/pipeline_viewer/views/index'; @import 'components/elasticsearch/shard_allocation/index'; +@import 'components/setup_mode/index'; diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js index 607edbd1e8709..239c6e3fd775a 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import { render } from 'react-dom'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { get, contains } from 'lodash'; import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; +import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; function isOnPage(hash) { return contains(window.location.hash, hash); @@ -21,15 +24,15 @@ const angularState = { const checkAngularState = () => { if (!angularState.injector || !angularState.scope) { - throw 'Unable to interact with setup mode because the angular injector was not previously set.' - + ' This needs to be set by calling `initSetupModeState`.'; + throw 'Unable to interact with setup mode because the angular injector was not previously set.' + + ' This needs to be set by calling `initSetupModeState`.'; } }; const setupModeState = { enabled: false, data: null, - callbacks: [] + callbacks: [], }; export const getSetupModeState = () => setupModeState; @@ -55,26 +58,23 @@ export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) let url = '../api/monitoring/v1/setup/collection'; if (uuid) { url += `/node/${uuid}`; - } - else if (!fetchWithoutClusterUuid && clusterUuid) { + } else if (!fetchWithoutClusterUuid && clusterUuid) { url += `/cluster/${clusterUuid}`; - } - else { + } else { url += '/cluster'; } try { const response = await http.post(url, { ccs }); return response.data; - } - catch (err) { + } catch (err) { const Private = angularState.injector.get('Private'); const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); } }; -const notifySetupModeDataChange = (oldData) => { +const notifySetupModeDataChange = oldData => { setupModeState.callbacks.forEach(cb => cb(oldData)); }; @@ -86,18 +86,21 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const isCloud = chrome.getInjected('isOnCloud'); const hasPermissions = get(data, '_meta.hasPermissions', false); if (isCloud || !hasPermissions) { - const text = !hasPermissions - ? i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { - defaultMessage: 'You do not have the necessary permissions to do this.' - }) - : i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', { - defaultMessage: 'This feature is not available on cloud.' + let text = null; + if (!hasPermissions) { + text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { + defaultMessage: 'You do not have the necessary permissions to do this.', }); + } else { + text = i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', { + defaultMessage: 'This feature is not available on cloud.', + }); + } angularState.scope.$evalAsync(() => { toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { - defaultMessage: 'Setup mode is not available' + defaultMessage: 'Setup mode is not available', }), text, }); @@ -110,8 +113,9 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const clusterUuid = globalState.cluster_uuid; if (!clusterUuid) { const liveClusterUuid = get(data, '_meta.liveClusterUuid'); - const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})) - .filter(node => node.isPartiallyMigrated || node.isFullyMigrated); + const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( + node => node.isPartiallyMigrated || node.isFullyMigrated + ); if (liveClusterUuid && migratedEsNodes.length > 0) { setNewlyDiscoveredClusterUuid(liveClusterUuid); } @@ -128,8 +132,7 @@ export const disableElasticsearchInternalCollection = async () => { try { const response = await http.post(url); return response.data; - } - catch (err) { + } catch (err) { const Private = angularState.injector.get('Private'); const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); return ajaxErrorHandlers(err); @@ -160,23 +163,12 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const navItems = []; - if (!globalState.inSetupMode && !chrome.getInjected('isOnCloud')) { - navItems.push({ - id: 'enter', - label: i18n.translate('xpack.monitoring.setupMode.enter', { - defaultMessage: 'Enter Setup Mode' - }), - run: () => toggleSetupMode(true), - testId: 'enterSetupMode' - }); - } + const enabled = !globalState.inSetupMode && !chrome.getInjected('isOnCloud'); - angularState.scope.topNavMenu = [...navItems]; - // LOL angular - if (!angularState.scope.$$phase) { - angularState.scope.$apply(); - } + render( + , + document.getElementById('setupModeNav') + ); }; export const initSetupModeState = async ($scope, $injector, callback) => { @@ -195,7 +187,7 @@ export const isInSetupMode = async () => { return true; } - const $injector = angularState.injector || await chrome.dangerouslyGetActiveInjector(); + const $injector = angularState.injector || (await chrome.dangerouslyGetActiveInjector()); const globalState = $injector.get('globalState'); return globalState.inSetupMode; }; diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js index 39ed049ab7492..1a9fdfeb920da 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -13,36 +13,40 @@ let setSetupModeMenuItem; jest.mock('./ajax_error_handler', () => ({ ajaxErrorHandlersProvider: err => { throw err; - } + }, +})); + +jest.mock('react-dom', () => ({ + render: jest.fn(), })); let data = {}; const injectorModulesMock = { globalState: { - save: jest.fn() + save: jest.fn(), }, Private: module => module, $http: { post: jest.fn().mockImplementation(() => { return { data }; - }) + }), }, $executor: { - run: jest.fn() - } + run: jest.fn(), + }, }; const angularStateMock = { injector: { get: module => { return injectorModulesMock[module] || {}; - } + }, }, scope: { $apply: fn => fn && fn(), - $evalAsync: fn => fn && fn() - } + $evalAsync: fn => fn && fn(), + }, }; // We are no longer waiting for setup mode data to be fetched when enabling @@ -66,11 +70,11 @@ function setModules() { describe('setup_mode', () => { beforeEach(async () => { jest.doMock('ui/chrome', () => ({ - getInjected: (key) => { + getInjected: key => { if (key === 'isOnCloud') { return false; } - } + }, })); setModules(); }); @@ -80,13 +84,14 @@ describe('setup_mode', () => { let error; try { toggleSetupMode(true); - } - catch (err) { + } catch (err) { error = err; } - expect(error).toEqual('Unable to interact with setup ' - + 'mode because the angular injector was not previously set. This needs to be ' - + 'set by calling `initSetupModeState`.'); + expect(error).toEqual( + 'Unable to interact with setup ' + + 'mode because the angular injector was not previously set. This needs to be ' + + 'set by calling `initSetupModeState`.' + ); }); it('should enable toggle mode', async () => { @@ -102,11 +107,11 @@ describe('setup_mode', () => { }); it('should set top nav config', async () => { + const render = require('react-dom').render; initSetupModeState(angularStateMock.scope, angularStateMock.injector); setSetupModeMenuItem(); - expect(angularStateMock.scope.topNavMenu.length).toBe(1); await toggleSetupMode(true); - expect(angularStateMock.scope.topNavMenu.length).toBe(0); + expect(render.mock.calls.length).toBe(2); }); }); @@ -115,32 +120,24 @@ describe('setup_mode', () => { data = {}; }); - it('should enable it through clicking top nav item', async () => { - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - setSetupModeMenuItem(); - expect(injectorModulesMock.globalState.inSetupMode).toBe(false); - await angularStateMock.scope.topNavMenu[0].run(); - expect(injectorModulesMock.globalState.inSetupMode).toBe(true); - }); - - it('should not fetch data if on cloud', async (done) => { + it('should not fetch data if on cloud', async done => { const addDanger = jest.fn(); jest.doMock('ui/chrome', () => ({ - getInjected: (key) => { + getInjected: key => { if (key === 'isOnCloud') { return true; } - } + }, })); data = { _meta: { - hasPermissions: true - } + hasPermissions: true, + }, }; jest.doMock('ui/notify', () => ({ toastNotifications: { addDanger, - } + }, })); setModules(); initSetupModeState(angularStateMock.scope, angularStateMock.injector); @@ -150,23 +147,23 @@ describe('setup_mode', () => { expect(state.enabled).toBe(false); expect(addDanger).toHaveBeenCalledWith({ title: 'Setup mode is not available', - text: 'This feature is not available on cloud.' + text: 'This feature is not available on cloud.', }); done(); }); }); - it('should not fetch data if the user does not have sufficient permissions', async (done) => { + it('should not fetch data if the user does not have sufficient permissions', async done => { const addDanger = jest.fn(); jest.doMock('ui/notify', () => ({ toastNotifications: { addDanger, - } + }, })); data = { _meta: { - hasPermissions: false - } + hasPermissions: false, + }, }; setModules(); initSetupModeState(angularStateMock.scope, angularStateMock.injector); @@ -176,26 +173,26 @@ describe('setup_mode', () => { expect(state.enabled).toBe(false); expect(addDanger).toHaveBeenCalledWith({ title: 'Setup mode is not available', - text: 'You do not have the necessary permissions to do this.' + text: 'You do not have the necessary permissions to do this.', }); done(); }); }); - it('should set the newly discovered cluster uuid', async (done) => { + it('should set the newly discovered cluster uuid', async done => { const clusterUuid = '1ajy'; data = { _meta: { liveClusterUuid: clusterUuid, - hasPermissions: true + hasPermissions: true, }, elasticsearch: { byUuid: { 123: { - isPartiallyMigrated: true - } - } - } + isPartiallyMigrated: true, + }, + }, + }, }; initSetupModeState(angularStateMock.scope, angularStateMock.injector); await toggleSetupMode(true); @@ -205,20 +202,20 @@ describe('setup_mode', () => { }); }); - it('should fetch data for a given cluster', async (done) => { + it('should fetch data for a given cluster', async done => { const clusterUuid = '1ajy'; data = { _meta: { liveClusterUuid: clusterUuid, - hasPermissions: true + hasPermissions: true, }, elasticsearch: { byUuid: { 123: { - isPartiallyMigrated: true - } - } - } + isPartiallyMigrated: true, + }, + }, + }, }; initSetupModeState(angularStateMock.scope, angularStateMock.injector); From bda4ea019568e2cf84bdb5298184a0554d4a8270 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Thu, 21 Nov 2019 14:25:23 -0700 Subject: [PATCH 003/128] [SIEM] [Detection Engine] Adds Rules Table (#50839) ## Summary This PR wires up the Detection Engine Rules Table and provides the following features: * [x] Lists all rules for a given user/space * [x] Search/Filtering via `Rule Name` * [x] Sorting via `Activate` * [x] Pagination * [x] Enable/Disable Action * [x] Rule Selection / Batch Actions * [x] Rule Import w/ validation via `io-ts` * [x] Batch Actions * [x] Activate selected * [x] Deactivate selected * [x] Export selected (as `.ndjson`) * [ ] ~Edit selected index patterns...~ (Waiting on supported feature) * [x] Delete selected * [x] Individual Overflow Actions * [ ] ~Edit rule settings~ (Waiting on supported feature) * [ ] ~Run rule manually...~ (Waiting on supported feature) * [x] Duplicate rule... * [X] Export rule * [x] Delete rule... ##### Searching / Sorting ![sort_and_filter](https://user-images.githubusercontent.com/2946766/69286404-641d1a80-0bb0-11ea-9930-8eada88b36f6.gif) ##### Importing / Exporting ![import_and_export](https://user-images.githubusercontent.com/2946766/69286806-79df0f80-0bb1-11ea-99c5-92df0a706f0e.gif) ##### Import Fails validation ![import_failed_validation](https://user-images.githubusercontent.com/2946766/69286797-72b80180-0bb1-11ea-9397-71fa0ff0b203.gif) ##### Batch Activate / Deactivate ![batch_activate_deactivate](https://user-images.githubusercontent.com/2946766/69287019-0093ec80-0bb2-11ea-8320-57cc7fec27a8.gif) ##### Batch Delete ![batch_delete](https://user-images.githubusercontent.com/2946766/69287139-6e401880-0bb2-11ea-948c-c5b92ba90e6f.gif) ##### Delete / Duplicate ![dupe_and_delete](https://user-images.githubusercontent.com/2946766/69287143-74ce9000-0bb2-11ea-88b3-db75f66ba666.gif) ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ -- * Will work with @benskelker on overall Detection Engine documentation - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios * Includes basic tests -- will expand coverage as features solidify - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ --- .../__snapshots__/utility_bar.test.tsx.snap | 6 +- .../utility_bar/utility_bar.test.tsx | 6 +- .../utility_bar/utility_bar_action.test.tsx | 2 +- .../utility_bar/utility_bar_action.tsx | 8 +- .../containers/detection_engine/rules/api.ts | 172 +++ .../detection_engine/rules/translations.ts | 11 + .../detection_engine/rules/types.ts | 81 ++ .../detection_engine/rules/use_rules.tsx | 83 ++ .../detection_engine/detection_engine.tsx | 6 +- .../detection_engine/rule_details/index.tsx | 6 +- .../rules/activity_monitor/columns.tsx | 87 ++ .../rules/activity_monitor/index.tsx | 337 ++++++ .../rules/all_rules/actions.tsx | 59 + .../rules/all_rules/batch_actions.tsx | 89 ++ .../rules/all_rules/columns.tsx | 167 +++ .../rules/all_rules/helpers.ts | 30 + .../rules/all_rules/index.tsx | 226 ++++ .../rules/all_rules/reducer.ts | 144 +++ .../__snapshots__/index.test.tsx.snap | 66 + .../import_rule_modal/index.test.tsx | 30 + .../components/import_rule_modal/index.tsx | 160 +++ .../import_rule_modal/translations.ts | 59 + .../__snapshots__/index.test.tsx.snap | 3 + .../components/json_downloader/index.test.tsx | 61 + .../components/json_downloader/index.tsx | 69 ++ .../__snapshots__/index.test.tsx.snap | 21 + .../components/rule_switch/index.test.tsx | 19 + .../rules/components/rule_switch/index.tsx | 55 + .../pages/detection_engine/rules/index.tsx | 1068 +---------------- .../detection_engine/rules/translations.ts | 196 +++ .../pages/detection_engine/rules/types.ts | 41 + 31 files changed, 2325 insertions(+), 1043 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap index 1f892acef7ef3..03a04983f9f86 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap @@ -12,11 +12,7 @@ exports[`UtilityBar it renders 1`] = ` - Test popover -

    - } + popoverContent={[Function]} > Test action
    diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx index 0ae247f5c9dd0..27688ec24530e 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx @@ -32,7 +32,7 @@ describe('UtilityBar', () => {
    - {'Test popover'}

    }> +

    {'Test popover'}

    }> {'Test action'}
    @@ -60,7 +60,7 @@ describe('UtilityBar', () => { - {'Test popover'}

    }> +

    {'Test popover'}

    }> {'Test action'}
    @@ -90,7 +90,7 @@ describe('UtilityBar', () => { - {'Test popover'}

    }> +

    {'Test popover'}

    }> {'Test action'}
    diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx index 74eed8cfabf2d..f71bdfda705d0 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx @@ -28,7 +28,7 @@ describe('UtilityBarAction', () => { test('it renders a popover', () => { const wrapper = mount( - {'Test popover'}

    }> +

    {'Test popover'}

    }> {'Test action'}
    diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx index ae4362bdbcd7b..2ad48bc9b9c92 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx @@ -5,7 +5,7 @@ */ import { EuiPopover } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { LinkIcon, LinkIconProps } from '../../link_icon'; import { BarAction } from './styles'; @@ -14,6 +14,8 @@ const Popover = React.memo( ({ children, color, iconSide, iconSize, iconType, popoverContent }) => { const [popoverState, setPopoverState] = useState(false); + const closePopover = useCallback(() => setPopoverState(false), [setPopoverState]); + return ( ( closePopover={() => setPopoverState(false)} isOpen={popoverState} > - {popoverContent} + {popoverContent?.(closePopover)} ); } @@ -38,7 +40,7 @@ const Popover = React.memo( Popover.displayName = 'Popover'; export interface UtilityBarActionProps extends LinkIconProps { - popoverContent?: React.ReactNode; + popoverContent?: (closePopover: () => void) => React.ReactNode; } export const UtilityBarAction = React.memo( diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts new file mode 100644 index 0000000000000..333baefe034fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import chrome from 'ui/chrome'; +import { + DeleteRulesProps, + DuplicateRulesProps, + EnableRulesProps, + FetchRulesProps, + FetchRulesResponse, + Rule, +} from './types'; +import { throwIfNotOk } from '../../../hooks/api/api'; + +/** + * Fetches all rules or single specified rule from the Detection Engine API + * + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param pagination desired pagination options (e.g. page/perPage) + * @param id if specified, will return specific rule if exists + * @param kbnVersion current Kibana Version to use for headers + */ +export const fetchRules = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + id, + kbnVersion, + signal, +}: FetchRulesProps): Promise => { + const queryParams = [ + `page=${pagination.page}`, + `per_page=${pagination.perPage}`, + `sort_field=${filterOptions.sortField}`, + `sort_order=${filterOptions.sortOrder}`, + ...(filterOptions.filter.length !== 0 + ? [`filter=alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`] + : []), + ]; + + const endpoint = + id != null + ? `${chrome.getBasePath()}/api/detection_engine/rules?id="${id}"` + : `${chrome.getBasePath()}/api/detection_engine/rules/_find?${queryParams.join('&')}`; + + const response = await fetch(endpoint, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + signal, + }); + await throwIfNotOk(response); + return id != null + ? { + page: 0, + perPage: 1, + total: 1, + data: response.json(), + } + : response.json(); +}; + +/** + * Enables/Disables provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to enable/disable + * @param enabled to enable or disable + * @param kbnVersion current Kibana Version to use for headers + */ +export const enableRules = async ({ + ids, + enabled, + kbnVersion, +}: EnableRulesProps): Promise => { + const requests = ids.map(id => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + method: 'PUT', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: JSON.stringify({ id, enabled }), + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; + +/** + * Deletes provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to delete + * @param kbnVersion current Kibana Version to use for headers + */ +export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promise => { + // TODO: Don't delete if immutable! + const requests = ids.map(id => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules?id=${id}`, { + method: 'DELETE', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; + +/** + * Duplicates provided Rules + * + * @param rule to duplicate + * @param kbnVersion current Kibana Version to use for headers + */ +export const duplicateRules = async ({ + rules, + kbnVersion, +}: DuplicateRulesProps): Promise => { + const requests = rules.map(rule => + fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: JSON.stringify({ + ...rule, + name: `${rule.name} [Duplicate]`, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_by: undefined, + enabled: rule.enabled, + }), + }) + ); + + const responses = await Promise.all(requests); + await responses.map(response => throwIfNotOk(response)); + return Promise.all( + responses.map>(response => response.json()) + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts new file mode 100644 index 0000000000000..a1ea2afb822f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const RULE_FETCH_FAILURE = i18n.translate('xpack.siem.containers.detectionEngine.rules', { + defaultMessage: 'Failed to fetch Rules', +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..afb0158fea677 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const RuleSchema = t.intersection([ + t.type({ + created_by: t.string, + description: t.string, + enabled: t.boolean, + id: t.string, + index: t.array(t.string), + interval: t.string, + language: t.string, + name: t.string, + query: t.string, + rule_id: t.string, + severity: t.string, + type: t.string, + updated_by: t.string, + }), + t.partial({ + false_positives: t.array(t.string), + from: t.string, + max_signals: t.number, + references: t.array(t.string), + tags: t.array(t.string), + to: t.string, + }), +]); + +export const RulesSchema = t.array(RuleSchema); + +export type Rule = t.TypeOf; +export type Rules = t.TypeOf; + +export interface PaginationOptions { + page: number; + perPage: number; + total: number; +} + +export interface FetchRulesProps { + pagination?: PaginationOptions; + filterOptions?: FilterOptions; + id?: string; + kbnVersion: string; + signal: AbortSignal; +} + +export interface FilterOptions { + filter: string; + sortField: string; + sortOrder: 'asc' | 'desc'; +} + +export interface FetchRulesResponse { + page: number; + perPage: number; + total: number; + data: Rule[]; +} + +export interface EnableRulesProps { + ids: string[]; + enabled: boolean; + kbnVersion: string; +} + +export interface DeleteRulesProps { + ids: string[]; + kbnVersion: string; +} + +export interface DuplicateRulesProps { + rules: Rules; + kbnVersion: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx new file mode 100644 index 0000000000000..2b8bb986a296a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { FetchRulesResponse, FilterOptions, PaginationOptions } from './types'; +import { useStateToaster } from '../../../components/toasters'; +import { fetchRules } from './api'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; + +type Return = [boolean, FetchRulesResponse]; + +/** + * Hook for using the list of Rules from the Detection Engine API + * + * @param pagination desired pagination options (e.g. page/perPage) + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param refetchToggle toggle for refetching data + */ +export const useRules = ( + pagination: PaginationOptions, + filterOptions: FilterOptions, + refetchToggle: boolean +): Return => { + const [rules, setRules] = useState({ + page: 1, + perPage: 20, + total: 0, + data: [], + }); + const [loading, setLoading] = useState(true); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function fetchData() { + try { + const fetchRulesResult = await fetchRules({ + filterOptions, + pagination, + kbnVersion, + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setRules(fetchRulesResult); + } + } catch (error) { + if (isSubscribed) { + errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setLoading(false); + } + } + + fetchData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [ + refetchToggle, + pagination.page, + pagination.perPage, + filterOptions.filter, + filterOptions.sortField, + filterOptions.sortOrder, + ]); + + return [loading, rules]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 9b63a6e160e42..f02e80ebfaf66 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -48,7 +48,7 @@ const OpenSignals = React.memo(() => { {'Batch actions context menu here.'}

    } + popoverContent={() =>

    {'Batch actions context menu here.'}

    } > {'Batch actions'}
    @@ -70,7 +70,7 @@ const OpenSignals = React.memo(() => { {'Customize columns context menu here.'}

    } + popoverContent={() =>

    {'Customize columns context menu here.'}

    } > {'Customize columns'}
    @@ -100,7 +100,7 @@ const ClosedSignals = React.memo(() => { {'Customize columns context menu here.'}

    } + popoverContent={() =>

    {'Customize columns context menu here.'}

    } > {'Customize columns'}
    diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx index da3e5fb2083dd..b16036e3142fc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx @@ -66,7 +66,7 @@ const OpenSignals = React.memo(() => { {'Batch actions context menu here.'}

    } + popoverContent={() =>

    {'Batch actions context menu here.'}

    } > {'Batch actions'}
    @@ -88,7 +88,7 @@ const OpenSignals = React.memo(() => { {'Customize columns context menu here.'}

    } + popoverContent={() =>

    {'Customize columns context menu here.'}

    } > {'Customize columns'}
    @@ -118,7 +118,7 @@ const ClosedSignals = React.memo(() => { {'Customize columns context menu here.'}

    } + popoverContent={() =>

    {'Customize columns context menu here.'}

    } > {'Customize columns'}
    diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx new file mode 100644 index 0000000000000..58e2b9f0cabc7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIconTip, EuiLink, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { ColumnTypes } from './index'; + +const actions = [ + { + available: (item: ColumnTypes) => item.status === 'Running', + description: 'Stop', + icon: 'stop', + isPrimary: true, + name: 'Stop', + onClick: () => {}, + type: 'icon', + }, + { + available: (item: ColumnTypes) => item.status === 'Stopped', + description: 'Resume', + icon: 'play', + isPrimary: true, + name: 'Resume', + onClick: () => {}, + type: 'icon', + }, +]; + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const columns = [ + { + field: 'rule', + name: 'Rule', + render: (value: ColumnTypes['rule']) => {value.name}, + sortable: true, + truncateText: true, + }, + { + field: 'ran', + name: 'Ran', + render: (value: ColumnTypes['ran']) => '--', + sortable: true, + truncateText: true, + }, + { + field: 'lookedBackTo', + name: 'Looked back to', + render: (value: ColumnTypes['lookedBackTo']) => '--', + sortable: true, + truncateText: true, + }, + { + field: 'status', + name: 'Status', + sortable: true, + truncateText: true, + }, + { + field: 'response', + name: 'Response', + render: (value: ColumnTypes['response']) => { + return value === undefined ? ( + getEmptyTagValue() + ) : ( + <> + {value === 'Fail' ? ( + + {value} + + ) : ( + {value} + )} + + ); + }, + sortable: true, + truncateText: true, + }, + { + actions, + width: '40px', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx new file mode 100644 index 0000000000000..d7306b8630bc2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx @@ -0,0 +1,337 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { useState } from 'react'; +import { HeaderSection } from '../../../../components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { columns } from './columns'; + +export interface RuleTypes { + href: string; + name: string; +} + +export interface ColumnTypes { + id: number; + rule: RuleTypes; + ran: string; + lookedBackTo: string; + status: string; + response: string | undefined; +} + +export interface PageTypes { + index: number; + size: number; +} + +export interface SortTypes { + field: string; + direction: string; +} + +export const ActivityMonitor = React.memo(() => { + const sampleTableData = [ + { + id: 1, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Running', + }, + { + id: 2, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Stopped', + }, + { + id: 3, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Fail', + }, + { + id: 4, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 5, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 6, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 7, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 8, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 9, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 10, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 11, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 12, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 13, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 14, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 15, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 16, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 17, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 18, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 19, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 20, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 21, + rule: { + href: '#/detection-engine/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + ]; + + const [itemsTotalState] = useState(sampleTableData.length); + const [pageState, setPageState] = useState({ index: 0, size: 20 }); + // const [selectedState, setSelectedState] = useState([]); + const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); + + return ( + <> + + + + + + + + + {'Showing: 39 activites'} + + + + {'Selected: 2 activities'} + + {'Stop selected'} + + + + {'Clear 7 filters'} + + + + + { + setPageState(page); + setSortState(sort); + }} + pagination={{ + pageIndex: pageState.index, + pageSize: pageState.size, + totalItemCount: itemsTotalState, + pageSizeOptions: [5, 10, 20], + }} + selection={{ + selectable: (item: ColumnTypes) => item.status !== 'Completed', + selectableMessage: (selectable: boolean) => + selectable ? undefined : 'Completed runs cannot be acted upon', + onSelectionChange: (selectedItems: ColumnTypes[]) => { + // setSelectedState(selectedItems); + }, + }} + sorting={{ + sort: sortState, + }} + /> + + + ); +}); +ActivityMonitor.displayName = 'ActivityMonitor'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx new file mode 100644 index 0000000000000..a54296f65a382 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + deleteRules, + duplicateRules, + enableRules, +} from '../../../../containers/detection_engine/rules/api'; +import { Action } from './reducer'; +import { Rule } from '../../../../containers/detection_engine/rules/types'; + +export const editRuleAction = () => {}; + +export const runRuleAction = () => {}; + +export const duplicateRuleAction = async ( + rule: Rule, + dispatch: React.Dispatch, + kbnVersion: string +) => { + dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true }); + const duplicatedRule = await duplicateRules({ rules: [rule], kbnVersion }); + dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false }); + dispatch({ type: 'updateRules', rules: duplicatedRule }); +}; + +export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch) => { + dispatch({ type: 'setExportPayload', exportPayload: rules }); +}; + +export const deleteRulesAction = async ( + ids: string[], + dispatch: React.Dispatch, + kbnVersion: string +) => { + dispatch({ type: 'updateLoading', ids, isLoading: true }); + const deletedRules = await deleteRules({ ids, kbnVersion }); + dispatch({ type: 'deleteRules', rules: deletedRules }); +}; + +export const enableRulesAction = async ( + ids: string[], + enabled: boolean, + dispatch: React.Dispatch, + kbnVersion: string +) => { + try { + dispatch({ type: 'updateLoading', ids, isLoading: true }); + const updatedRules = await enableRules({ ids, enabled, kbnVersion }); + dispatch({ type: 'updateRules', rules: updatedRules }); + } catch { + // TODO Add error toast support to actions (and @throw jsdoc to api calls) + dispatch({ type: 'updateLoading', ids, isLoading: false }); + } +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx new file mode 100644 index 0000000000000..c8fb9d98fde6a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import * as i18n from '../translations'; +import { TableData } from '../types'; +import { Action } from './reducer'; +import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions'; + +export const getBatchItems = ( + selectedState: TableData[], + dispatch: React.Dispatch, + closePopover: () => void, + kbnVersion: string +) => { + const containsEnabled = selectedState.some(v => v.activate); + const containsDisabled = selectedState.some(v => !v.activate); + const containsLoading = selectedState.some(v => v.isLoading); + + return [ + { + closePopover(); + const deactivatedIds = selectedState.filter(s => !s.activate).map(s => s.id); + await enableRulesAction(deactivatedIds, true, dispatch, kbnVersion); + }} + > + {i18n.BATCH_ACTION_ACTIVATE_SELECTED} + , + { + closePopover(); + const activatedIds = selectedState.filter(s => s.activate).map(s => s.id); + await enableRulesAction(activatedIds, false, dispatch, kbnVersion); + }} + > + {i18n.BATCH_ACTION_DEACTIVATE_SELECTED} + , + { + closePopover(); + await exportRulesAction( + selectedState.map(s => s.sourceRule), + dispatch + ); + }} + > + {i18n.BATCH_ACTION_EXPORT_SELECTED} + , + { + closePopover(); + }} + > + {i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS} + , + { + closePopover(); + await deleteRulesAction( + selectedState.map(({ sourceRule: { id } }) => id), + dispatch, + kbnVersion + ); + }} + > + {i18n.BATCH_ACTION_DELETE_SELECTED} + , + ]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx new file mode 100644 index 0000000000000..cae0fb3eaf906 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge, EuiHealth, EuiIconTip, EuiLink, EuiTextColor } from '@elastic/eui'; +import React from 'react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { + deleteRulesAction, + duplicateRuleAction, + editRuleAction, + enableRulesAction, + exportRulesAction, + runRuleAction, +} from './actions'; + +import { Action } from './reducer'; +import { TableData } from '../types'; +import * as i18n from '../translations'; +import { PreferenceFormattedDate } from '../../../../components/formatted_date'; +import { RuleSwitch } from '../components/rule_switch'; + +const getActions = (dispatch: React.Dispatch, kbnVersion: string) => [ + { + description: i18n.EDIT_RULE_SETTINGS, + icon: 'visControls', + name: i18n.EDIT_RULE_SETTINGS, + onClick: editRuleAction, + enabled: () => false, + }, + { + description: i18n.RUN_RULE_MANUALLY, + icon: 'play', + name: i18n.RUN_RULE_MANUALLY, + onClick: runRuleAction, + enabled: () => false, + }, + { + description: i18n.DUPLICATE_RULE, + icon: 'copy', + name: i18n.DUPLICATE_RULE, + onClick: (rowItem: TableData) => duplicateRuleAction(rowItem.sourceRule, dispatch, kbnVersion), + }, + { + description: i18n.EXPORT_RULE, + icon: 'exportAction', + name: i18n.EXPORT_RULE, + onClick: (rowItem: TableData) => exportRulesAction([rowItem.sourceRule], dispatch), + }, + { + description: i18n.DELETE_RULE, + icon: 'trash', + name: i18n.DELETE_RULE, + onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, kbnVersion), + }, +]; + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const getColumns = (dispatch: React.Dispatch, kbnVersion: string) => [ + { + field: 'rule', + name: i18n.COLUMN_RULE, + render: (value: TableData['rule']) => {value.name}, + truncateText: true, + width: '24%', + }, + { + field: 'method', + name: i18n.COLUMN_METHOD, + truncateText: true, + }, + { + field: 'severity', + name: i18n.COLUMN_SEVERITY, + render: (value: TableData['severity']) => ( + + {value} + + ), + truncateText: true, + }, + { + field: 'lastCompletedRun', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: TableData['lastCompletedRun']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + ); + }, + sortable: true, + truncateText: true, + width: '16%', + }, + { + field: 'lastResponse', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: TableData['lastResponse']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + <> + {value.type === 'Fail' ? ( + + {value.type} + + ) : ( + {value.type} + )} + + ); + }, + truncateText: true, + }, + { + field: 'tags', + name: i18n.COLUMN_TAGS, + render: (value: TableData['tags']) => ( +
    + <> + {value.map((tag, i) => ( + + {tag} + + ))} + +
    + ), + truncateText: true, + width: '20%', + }, + { + align: 'center', + field: 'activate', + name: i18n.COLUMN_ACTIVATE, + render: (value: TableData['activate'], item: TableData) => ( + { + await enableRulesAction([id], enabled, dispatch, kbnVersion); + }} + /> + ), + sortable: true, + width: '85px', + }, + { + actions: getActions(dispatch, kbnVersion), + width: '40px', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts new file mode 100644 index 0000000000000..db02d41771f68 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Rule } from '../../../../containers/detection_engine/rules/types'; +import { TableData } from '../types'; +import { getEmptyValue } from '../../../../components/empty_value'; + +export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] => + rules.map(rule => ({ + id: rule.id, + rule_id: rule.rule_id, + rule: { + href: `#/detection-engine/rules/rule-details/${encodeURIComponent(rule.id)}`, + name: rule.name, + status: 'Status Placeholder', + }, + method: rule.type, // TODO: Map to i18n? + severity: rule.severity, + lastCompletedRun: undefined, // TODO: Not available yet + lastResponse: { + type: getEmptyValue(), // TODO: Not available yet + }, + tags: rule.tags ?? [], + activate: rule.enabled, + sourceRule: rule, + isLoading: selectedIds?.includes(rule.id) ?? false, + })); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx new file mode 100644 index 0000000000000..a73ebeb61db3c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBasicTable, + EuiContextMenuPanel, + EuiFieldSearch, + EuiLoadingContent, + EuiSpacer, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useReducer, useState } from 'react'; + +import uuid from 'uuid'; +import { HeaderSection } from '../../../../components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { getColumns } from './columns'; +import { useRules } from '../../../../containers/detection_engine/rules/use_rules'; +import { Loader } from '../../../../components/loader'; +import { Panel } from '../../../../components/panel'; +import { getBatchItems } from './batch_actions'; +import { EuiBasicTableOnChange, TableData } from '../types'; +import { allRulesReducer, State } from './reducer'; +import * as i18n from '../translations'; +import { useKibanaUiSetting } from '../../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../../common/constants'; +import { JSONDownloader } from '../components/json_downloader'; +import { useStateToaster } from '../../../../components/toasters'; + +const initialState: State = { + isLoading: true, + rules: [], + tableData: [], + selectedItems: [], + refreshToggle: true, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + }, +}; + +/** + * Table Component for displaying all Rules for a given cluster. Provides the ability to filter + * by name, sort by enabled, and perform the following actions: + * * Enable/Disable + * * Duplicate + * * Delete + * * Import/Export + */ +export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importCompleteToggle => { + const [ + { + exportPayload, + filterOptions, + isLoading, + refreshToggle, + selectedItems, + tableData, + pagination, + }, + dispatch, + ] = useReducer(allRulesReducer, initialState); + + const [isInitialLoad, setIsInitialLoad] = useState(true); + const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedItems, dispatch, kbnVersion] + ); + + useEffect(() => { + dispatch({ type: 'loading', isLoading: isLoadingRules }); + + if (!isLoadingRules) { + setIsInitialLoad(false); + } + }, [isLoadingRules]); + + useEffect(() => { + if (!isInitialLoad) { + dispatch({ type: 'refresh' }); + } + }, [importCompleteToggle]); + + useEffect(() => { + dispatch({ + type: 'updateRules', + rules: rulesData.data, + pagination: { + page: rulesData.page, + perPage: rulesData.perPage, + total: rulesData.total, + }, + }); + }, [rulesData]); + + return ( + <> + { + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), + color: 'success', + iconType: 'check', + }, + }); + }} + /> + + + + {isInitialLoad ? ( + + ) : ( + <> + + { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + filter: filterString, + }, + }); + }} + /> + + + + + + {i18n.SHOWING_RULES(pagination.total ?? 0)} + + + + {i18n.SELECTED_RULES(selectedItems.length)} + + {i18n.BATCH_ACTIONS} + + dispatch({ type: 'refresh' })} + > + {i18n.REFRESH} + + + + + + { + dispatch({ + type: 'updatePagination', + pagination: { ...pagination, page: page.index + 1, perPage: page.size }, + }); + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...filterOptions, + sortField: 'enabled', // Only enabled is supported for sorting currently + sortOrder: sort.direction, + }, + }); + }} + pagination={{ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total, + pageSizeOptions: [5, 10, 20], + }} + selection={{ + selectable: (item: TableData) => !item.isLoading, + onSelectionChange: (selected: TableData[]) => + dispatch({ type: 'setSelected', selectedItems: selected }), + }} + sorting={{ sort: { field: 'activate', direction: filterOptions.sortOrder } }} + /> + {isLoading && } + + )} + + + ); +}); + +AllRules.displayName = 'AllRules'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts new file mode 100644 index 0000000000000..c59c5687c10c9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/reducer.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FilterOptions, + PaginationOptions, + Rule, +} from '../../../../containers/detection_engine/rules/types'; +import { TableData } from '../types'; +import { formatRules } from './helpers'; + +export interface State { + isLoading: boolean; + rules: Rule[]; + selectedItems: TableData[]; + pagination: PaginationOptions; + filterOptions: FilterOptions; + refreshToggle: boolean; + tableData: TableData[]; + exportPayload?: object[]; +} + +export type Action = + | { type: 'refresh' } + | { type: 'loading'; isLoading: boolean } + | { type: 'deleteRules'; rules: Rule[] } + | { type: 'duplicate'; rule: Rule } + | { type: 'setExportPayload'; exportPayload?: object[] } + | { type: 'setSelected'; selectedItems: TableData[] } + | { type: 'updateLoading'; ids: string[]; isLoading: boolean } + | { type: 'updateRules'; rules: Rule[]; pagination?: PaginationOptions } + | { type: 'updatePagination'; pagination: PaginationOptions } + | { type: 'updateFilterOptions'; filterOptions: FilterOptions } + | { type: 'failure' }; + +export const allRulesReducer = (state: State, action: Action): State => { + switch (action.type) { + case 'refresh': { + return { + ...state, + refreshToggle: !state.refreshToggle, + }; + } + case 'updateRules': { + // If pagination included, this was a hard refresh + if (action.pagination) { + return { + ...state, + rules: action.rules, + pagination: action.pagination, + tableData: formatRules(action.rules), + }; + } + + const ruleIds = state.rules.map(r => r.rule_id); + const updatedRules = action.rules.reduce( + (rules, updatedRule) => + ruleIds.includes(updatedRule.rule_id) + ? rules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r)) + : [...rules, updatedRule], + [...state.rules] + ); + + // Update enabled on selectedItems so that batch actions show correct available actions + const updatedRuleIdToState = action.rules.reduce>( + (acc, r) => ({ ...acc, [r.id]: r.enabled }), + {} + ); + const updatedSelectedItems = state.selectedItems.map(selectedItem => + Object.keys(updatedRuleIdToState).includes(selectedItem.id) + ? { ...selectedItem, activate: updatedRuleIdToState[selectedItem.id] } + : selectedItem + ); + + return { + ...state, + rules: updatedRules, + tableData: formatRules(updatedRules), + selectedItems: updatedSelectedItems, + }; + } + case 'updatePagination': { + return { + ...state, + pagination: action.pagination, + }; + } + case 'updateFilterOptions': { + return { + ...state, + filterOptions: action.filterOptions, + }; + } + case 'deleteRules': { + const deletedRuleIds = action.rules.map(r => r.rule_id); + const updatedRules = state.rules.reduce( + (rules, rule) => (deletedRuleIds.includes(rule.rule_id) ? rules : [...rules, rule]), + [] + ); + return { + ...state, + rules: updatedRules, + tableData: formatRules(updatedRules), + }; + } + case 'setSelected': { + return { + ...state, + selectedItems: action.selectedItems, + }; + } + case 'updateLoading': { + return { + ...state, + rules: state.rules, + tableData: formatRules(state.rules, action.ids), + }; + } + case 'loading': { + return { + ...state, + isLoading: action.isLoading, + }; + } + case 'failure': { + return { + ...state, + isLoading: false, + rules: [], + }; + } + case 'setExportPayload': { + return { + ...state, + exportPayload: action.exportPayload, + }; + } + default: + return state; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..6b0aa02d4edfa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ImportRuleModal renders correctly against snapshot 1`] = ` + + + + + + Import rule + + + + +

    + Select a SIEM rule (as exported from the Detection Engine UI) to import +

    +
    + + + + +
    + + + Cancel + + + Import rule + + +
    +
    +
    +`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx new file mode 100644 index 0000000000000..b397e50201f14 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { ImportRuleModal } from './index'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import { getMockKibanaUiSetting, MockFrameworks } from '../../../../../mock'; +import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; + +const mockUseKibanaUiSetting: jest.Mock = useKibanaUiSetting as jest.Mock; +jest.mock('../../../../../lib/settings/use_kibana_ui_setting', () => ({ + useKibanaUiSetting: jest.fn(), +})); + +describe('ImportRuleModal', () => { + test('renders correctly against snapshot', () => { + mockUseKibanaUiSetting.mockImplementation( + getMockKibanaUiSetting((DEFAULT_KBN_VERSION as unknown) as MockFrameworks) + ); + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx new file mode 100644 index 0000000000000..fdcf6263f414f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiCheckbox, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + // @ts-ignore no-exported-member + EuiFilePicker, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import React, { useCallback, useState } from 'react'; +import { failure } from 'io-ts/lib/PathReporter'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import uuid from 'uuid'; +import * as i18n from './translations'; +import { duplicateRules } from '../../../../../containers/detection_engine/rules/api'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; +import { ndjsonToJSON } from '../json_downloader'; +import { RulesSchema } from '../../../../../containers/detection_engine/rules/types'; +import { useStateToaster } from '../../../../../components/toasters'; + +interface ImportRuleModalProps { + showModal: boolean; + closeModal: () => void; + importComplete: () => void; +} + +/** + * Modal component for importing Rules from a json file + * + * @param filename name of file to be downloaded + * @param payload JSON string to write to file + * + */ +export const ImportRuleModal = React.memo( + ({ showModal, closeModal, importComplete }) => { + const [selectedFiles, setSelectedFiles] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + const cleanupAndCloseModal = () => { + setIsImporting(false); + setSelectedFiles(null); + closeModal(); + }; + + const importRules = useCallback(async () => { + if (selectedFiles != null) { + setIsImporting(true); + const reader = new FileReader(); + reader.onload = async event => { + // @ts-ignore type is string, not ArrayBuffer as FileReader.readAsText is called + const importedRules = ndjsonToJSON(event?.target?.result ?? ''); + + const decodedRules = pipe( + RulesSchema.decode(importedRules), + fold(errors => { + cleanupAndCloseModal(); + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.IMPORT_FAILED, + color: 'danger', + iconType: 'alert', + errors: failure(errors), + }, + }); + throw new Error(failure(errors).join('\n')); + }, identity) + ); + + const duplicatedRules = await duplicateRules({ rules: decodedRules, kbnVersion }); + importComplete(); + cleanupAndCloseModal(); + + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_IMPORTED_RULES(duplicatedRules.length), + color: 'success', + iconType: 'check', + }, + }); + }; + Object.values(selectedFiles).map(f => reader.readAsText(f)); + } + }, [selectedFiles]); + + return ( + <> + {showModal && ( + + + + {i18n.IMPORT_RULE} + + + + +

    {i18n.SELECT_RULE}

    +
    + + + { + setSelectedFiles(Object.keys(files).length > 0 ? files : null); + }} + display={'large'} + fullWidth={true} + isLoading={isImporting} + /> + + {}} + /> +
    + + + {i18n.CANCEL_BUTTON} + + {i18n.IMPORT_RULE} + + +
    +
    + )} + + ); + } +); + +ImportRuleModal.displayName = 'ImportRuleModal'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts new file mode 100644 index 0000000000000..50c3c75b6109f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/translations.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const IMPORT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.importRuleTitle', + { + defaultMessage: 'Import rule', + } +); + +export const SELECT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.selectRuleDescription', + { + defaultMessage: 'Select a SIEM rule (as exported from the Detection Engine UI) to import', + } +); + +export const INITIAL_PROMPT_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.initialPromptTextDescription', + { + defaultMessage: 'Select or drag and drop files', + } +); + +export const OVERWRITE_WITH_SAME_NAME = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.overwriteDescription', + { + defaultMessage: 'Automatically overwrite saved objects with the same name', + } +); + +export const CANCEL_BUTTON = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.cancelTitle', + { + defaultMessage: 'Cancel', + } +); + +export const SUCCESSFULLY_IMPORTED_RULES = (totalRules: number) => + i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle', + { + values: { totalRules }, + defaultMessage: + 'Successfully imported {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + } + ); + +export const IMPORT_FAILED = i18n.translate( + 'xpack.siem.detectionEngine.components.importRuleModal.importFailedTitle', + { + defaultMessage: 'Failed to import rules', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..c4377c265c2c2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/__snapshots__/index.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JSONDownloader renders correctly against snapshot 1`] = ``; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx new file mode 100644 index 0000000000000..ef6493f89f383 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { JSONDownloader, jsonToNDJSON, ndjsonToJSON } from './index'; + +const jsonArray = [ + { + description: 'Detecting root and admin users1', + created_by: 'elastic', + false_positives: [], + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + max_signals: 100, + }, + { + description: 'Detecting root and admin users2', + created_by: 'elastic', + false_positives: [], + index: ['auditbeat-*', 'packetbeat-*', 'winlogbeat-*'], + max_signals: 101, + }, +]; + +const ndjson = `{"description":"Detecting root and admin users1","created_by":"elastic","false_positives":[],"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"max_signals":100} +{"description":"Detecting root and admin users2","created_by":"elastic","false_positives":[],"index":["auditbeat-*","packetbeat-*","winlogbeat-*"],"max_signals":101}`; + +const ndjsonSorted = `{"created_by":"elastic","description":"Detecting root and admin users1","false_positives":[],"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"max_signals":100} +{"created_by":"elastic","description":"Detecting root and admin users2","false_positives":[],"index":["auditbeat-*","packetbeat-*","winlogbeat-*"],"max_signals":101}`; + +describe('JSONDownloader', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); + + describe('jsonToNDJSON', () => { + test('converts to NDJSON', () => { + const output = jsonToNDJSON(jsonArray, false); + expect(output).toEqual(ndjson); + }); + + test('converts to NDJSON with keys sorted', () => { + const output = jsonToNDJSON(jsonArray); + expect(output).toEqual(ndjsonSorted); + }); + }); + + describe('ndjsonToJSON', () => { + test('converts to JSON', () => { + const output = ndjsonToJSON(ndjson); + expect(output).toEqual(jsonArray); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx new file mode 100644 index 0000000000000..e9c2c69f067cc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useRef } from 'react'; +import styled from 'styled-components'; + +const InvisibleAnchor = styled.a` + display: none; +`; + +export interface JSONDownloaderProps { + filename: string; + payload?: object[]; + onExportComplete: (exportCount: number) => void; +} + +/** + * Component for downloading JSON as a file. Download will occur on each update to `payload` param + * + * @param filename name of file to be downloaded + * @param payload JSON string to write to file + * + */ +export const JSONDownloader = React.memo( + ({ filename, payload, onExportComplete }) => { + const anchorRef = useRef(null); + + useEffect(() => { + if (anchorRef && anchorRef.current && payload != null) { + const blob = new Blob([jsonToNDJSON(payload)], { type: 'application/json' }); + // @ts-ignore function is not always defined -- this is for supporting IE + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob); + } else { + const objectURL = window.URL.createObjectURL(blob); + anchorRef.current.href = objectURL; + anchorRef.current.download = filename; + anchorRef.current.click(); + window.URL.revokeObjectURL(objectURL); + } + onExportComplete(payload.length); + } + }, [payload]); + + return ; + } +); + +JSONDownloader.displayName = 'JSONDownloader'; + +export const jsonToNDJSON = (jsonArray: object[], sortKeys = true): string => { + return jsonArray + .map(j => JSON.stringify(j, sortKeys ? Object.keys(j).sort() : null, 0)) + .join('\n'); +}; + +export const ndjsonToJSON = (ndjson: string): object[] => { + const jsonLines = ndjson.split(/\r?\n/); + return jsonLines.reduce((acc, line) => { + try { + return [...acc, JSON.parse(line)]; + } catch (e) { + return acc; + } + }, []); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..98f8ae6a80e07 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RuleSwitch renders correctly against snapshot 1`] = ` + + + + + +`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx new file mode 100644 index 0000000000000..9e5f4317678e8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import * as React from 'react'; +import { RuleSwitch } from './index'; + +describe('RuleSwitch', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(toJson(wrapper)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx new file mode 100644 index 0000000000000..da58b2e076e0d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; + +const StaticSwitch = styled(EuiSwitch)` + .euiSwitch__thumb, + .euiSwitch__icon { + transition: none; + } +`; + +StaticSwitch.displayName = 'StaticSwitch'; + +export interface RuleSwitchProps { + id: string; + enabled: boolean; + isLoading: boolean; + onRuleStateChange: (isEnabled: boolean, id: string) => void; +} + +/** + * Basic switch component for displaying loader when enabled/disabled + */ +export const RuleSwitch = React.memo( + ({ id, enabled, isLoading, onRuleStateChange }) => { + return ( + + + {isLoading ? ( + + ) : ( + { + onRuleStateChange(e.target.checked!, id); + }} + /> + )} + + + ); + } +); + +RuleSwitch.displayName = 'RuleSwitch'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 9d2f5dc0a6c29..afff0f07dfac4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -4,1049 +4,66 @@ * you may not use this file except in compliance with the Elastic License. */ -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { - EuiBadge, - EuiBasicTable, - EuiButton, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiHealth, - EuiIconTip, - EuiLink, - EuiPanel, - EuiSpacer, - EuiSwitch, - EuiTabbedContent, - EuiTextColor, -} from '@elastic/eui'; -import moment from 'moment'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui'; import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; -import { getEmptyTagValue } from '../../../components/empty_value'; import { HeaderPage } from '../../../components/header_page'; -import { HeaderSection } from '../../../components/header_section'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../components/detection_engine/utility_bar'; + import { WrapperPage } from '../../../components/wrapper_page'; import { SpyRoute } from '../../../utils/route/spy_routes'; import * as i18n from './translations'; - -// Michael: Will need to change this to get the current datetime format from Kibana settings. -const dateTimeFormat = (value: string) => { - return moment(value).format('M/D/YYYY, h:mm A'); -}; - -const AllRules = React.memo(() => { - interface RuleTypes { - href: string; - name: string; - status: string; - } - - interface LastResponseTypes { - type: string; - message?: string; - } - - interface ColumnTypes { - id: number; - rule: RuleTypes; - method: string; - severity: string; - lastCompletedRun: string; - lastResponse: LastResponseTypes; - tags: string | string[]; - activate: boolean; - } - - interface PageTypes { - index: number; - size: number; - } - - interface SortTypes { - field: string; - direction: string; - } - - const actions = [ - { - description: 'Edit rule settings', - icon: 'visControls', - name: 'Edit rule settings', - onClick: () => {}, - }, - { - description: 'Run rule manually…', - icon: 'play', - name: 'Run rule manually…', - onClick: () => {}, - }, - { - description: 'Duplicate rule…', - icon: 'copy', - name: 'Duplicate rule…', - onClick: () => {}, - }, - { - description: 'Export rule', - icon: 'exportAction', - name: 'Export rule', - onClick: () => {}, - }, - { - description: 'Delete rule…', - icon: 'trash', - name: 'Delete rule…', - onClick: () => {}, - }, - ]; - - // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? - const columns = [ - { - field: 'rule', - name: 'Rule', - render: (value: ColumnTypes['rule']) => ( -
    - {value.name}{' '} - {value.status} -
    - ), - sortable: true, - truncateText: true, - width: '24%', - }, - { - field: 'method', - name: 'Method', - sortable: true, - truncateText: true, - }, - { - field: 'severity', - name: 'Severity', - render: (value: ColumnTypes['severity']) => ( - - {value} - - ), - sortable: true, - truncateText: true, - }, - { - field: 'lastCompletedRun', - name: 'Last completed run', - render: (value: ColumnTypes['lastCompletedRun']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - - ); - }, - sortable: true, - truncateText: true, - width: '16%', - }, - { - field: 'lastResponse', - name: 'Last response', - render: (value: ColumnTypes['lastResponse']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value.type === 'Fail' ? ( - - {value.type} - - ) : ( - {value.type} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - field: 'tags', - name: 'Tags', - render: (value: ColumnTypes['tags']) => ( -
    - {typeof value !== 'string' ? ( - <> - {value.map((tag, i) => ( - - {tag} - - ))} - - ) : ( - {value} - )} -
    - ), - sortable: true, - truncateText: true, - width: '20%', - }, - { - align: 'center', - field: 'activate', - name: 'Activate', - render: (value: ColumnTypes['activate']) => ( - {}} showLabel={false} /> - ), - sortable: true, - width: '65px', - }, - { - actions, - width: '40px', - }, - ]; - - const sampleTableData = [ - { - id: 1, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Low', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: ['attack.t1234', 'attack.t4321'], - activate: true, - }, - { - id: 2, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Medium', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Fail', - message: 'Full fail message here.', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 3, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'High', - tags: 'attack.t1234', - activate: false, - }, - { - id: 4, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 5, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 6, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 7, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 8, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 9, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 10, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 11, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 12, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 13, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 14, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 15, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 16, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 17, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 18, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 19, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 20, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - { - id: 21, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - status: 'Experimental', - }, - method: 'Custom query', - severity: 'Critical', - lastCompletedRun: '2019-12-28 00:00:00.000-05:00', - lastResponse: { - type: 'Success', - }, - tags: 'attack.t1234', - activate: true, - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'rule', direction: 'asc' }); - - return ( - <> - - - - - - - - - - - {'Showing: 39 rules'} - - - - {'Selected: 2 rules'} - - {'Batch actions context menu here.'}

    } - > - {'Batch actions'} -
    -
    - - - {'Clear 7 filters'} - -
    -
    - - { - setPageState(page); - setSortState(sort); - }} - pagination={{ - pageIndex: pageState.index, - pageSize: pageState.size, - totalItemCount: itemsTotalState, - pageSizeOptions: [5, 10, 20], - }} - selection={{ - selectable: () => true, - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> -
    - - ); -}); -AllRules.displayName = 'AllRules'; - -const ActivityMonitor = React.memo(() => { - interface RuleTypes { - href: string; - name: string; - } - - interface ColumnTypes { - id: number; - rule: RuleTypes; - ran: string; - lookedBackTo: string; - status: string; - response: string | undefined; - } - - interface PageTypes { - index: number; - size: number; - } - - interface SortTypes { - field: string; - direction: string; - } - - const actions = [ - { - available: (item: ColumnTypes) => item.status === 'Running', - description: 'Stop', - icon: 'stop', - isPrimary: true, - name: 'Stop', - onClick: () => {}, - type: 'icon', - }, - { - available: (item: ColumnTypes) => item.status === 'Stopped', - description: 'Resume', - icon: 'play', - isPrimary: true, - name: 'Resume', - onClick: () => {}, - type: 'icon', - }, - ]; - - // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? - const columns = [ - { - field: 'rule', - name: 'Rule', - render: (value: ColumnTypes['rule']) => {value.name}, - sortable: true, - truncateText: true, - }, - { - field: 'ran', - name: 'Ran', - render: (value: ColumnTypes['ran']) => , - sortable: true, - truncateText: true, - }, - { - field: 'lookedBackTo', - name: 'Looked back to', - render: (value: ColumnTypes['lookedBackTo']) => ( - - ), - sortable: true, - truncateText: true, - }, - { - field: 'status', - name: 'Status', - sortable: true, - truncateText: true, - }, - { - field: 'response', - name: 'Response', - render: (value: ColumnTypes['response']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value === 'Fail' ? ( - - {value} - - ) : ( - {value} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - actions, - width: '40px', - }, - ]; - - const sampleTableData = [ - { - id: 1, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Running', - }, - { - id: 2, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Stopped', - }, - { - id: 3, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Fail', - }, - { - id: 4, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 5, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 6, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 7, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 8, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 9, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 10, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 11, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 12, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 13, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 14, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 15, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 16, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 17, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 18, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 19, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 20, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 21, - rule: { - href: '#/detection-engine/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); - - return ( - <> - - - - - - - - - {'Showing: 39 activites'} - - - - {'Selected: 2 activities'} - - {'Stop selected'} - - - - {'Clear 7 filters'} - - - - - { - setPageState(page); - setSortState(sort); - }} - pagination={{ - pageIndex: pageState.index, - pageSize: pageState.size, - totalItemCount: itemsTotalState, - pageSizeOptions: [5, 10, 20], - }} - selection={{ - selectable: (item: ColumnTypes) => item.status !== 'Completed', - selectableMessage: (selectable: boolean) => - selectable ? undefined : 'Completed runs cannot be acted upon', - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> - - - ); -}); -ActivityMonitor.displayName = 'ActivityMonitor'; +import { AllRules } from './all_rules'; +import { ActivityMonitor } from './activity_monitor'; +import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; +import { getEmptyTagValue } from '../../../components/empty_value'; +import { ImportRuleModal } from './components/import_rule_modal'; export const RulesComponent = React.memo(() => { + const [showImportModal, setShowImportModal] = useState(false); + const [importCompleteToggle, setImportCompleteToggle] = useState(false); + + const lastCompletedRun = undefined; return ( <> + setShowImportModal(false)} + importComplete={() => setImportCompleteToggle(!importCompleteToggle)} + /> , + }} + /> + ) : ( + getEmptyTagValue() + ) + } title={i18n.PAGE_TITLE} > - - {'Import rule…'} + { + setShowImportModal(true); + }} + > + {i18n.IMPORT_RULE} - {'Add new rule'} + {i18n.ADD_NEW_RULE} @@ -1056,12 +73,12 @@ export const RulesComponent = React.memo(() => { tabs={[ { id: 'tabAllRules', - name: 'All rules', - content: , + name: i18n.ALL_RULES, + content: , }, { id: 'tabActivityMonitor', - name: 'Activity monitor', + name: i18n.ACTIVITY_MONITOR, content: , }, ]} @@ -1072,4 +89,5 @@ export const RulesComponent = React.memo(() => { ); }); + RulesComponent.displayName = 'RulesComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index 2b20c726d4b3f..9ae266e396f6d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -6,6 +6,202 @@ import { i18n } from '@kbn/i18n'; +export const BACK_TO_DETECTION_ENGINE = i18n.translate( + 'xpack.siem.detectionEngine.rules.backOptionsHeader', + { + defaultMessage: 'Back to detection engine', + } +); + +export const IMPORT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.importRuleTitle', { + defaultMessage: 'Import rule…', +}); + +export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.addNewRuleTitle', { + defaultMessage: 'Add new rule', +}); + +export const ACTIVITY_MONITOR = i18n.translate( + 'xpack.siem.detectionEngine.rules.activityMonitorTitle', + { + defaultMessage: 'Activity monitor', + } +); + export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', { defaultMessage: 'Rules', }); + +export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules.refreshTitle', { + defaultMessage: 'Refresh', +}); + +export const BATCH_ACTIONS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActionsTitle', + { + defaultMessage: 'Batch actions', + } +); + +export const BATCH_ACTION_ACTIVATE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.activateSelectedTitle', + { + defaultMessage: 'Activate selected', + } +); + +export const BATCH_ACTION_DEACTIVATE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle', + { + defaultMessage: 'Deactivate selected', + } +); + +export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.exportSelectedTitle', + { + defaultMessage: 'Export selected', + } +); + +export const BATCH_ACTION_EDIT_INDEX_PATTERNS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.editIndexPatternsTitle', + { + defaultMessage: 'Edit selected index patterns…', + } +); + +export const BATCH_ACTION_DELETE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle', + { + defaultMessage: 'Delete selected…', + } +); + +export const EXPORT_FILENAME = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.exportFilenameTitle', + { + defaultMessage: 'rules_export', + } +); + +export const SUCCESSFULLY_EXPORTED_RULES = (totalRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.successfullyExportedRulesTitle', { + values: { totalRules }, + defaultMessage: + 'Successfully exported {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + }); + +export const ALL_RULES = i18n.translate('xpack.siem.detectionEngine.rules.allRules.tableTitle', { + defaultMessage: 'All rules', +}); + +export const SEARCH_RULES = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.searchAriaLabel', + { + defaultMessage: 'Search rules', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.searchPlaceholder', + { + defaultMessage: 'e.g. rule name', + } +); + +export const SHOWING_RULES = (totalRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.showingRulesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + }); + +export const SELECTED_RULES = (selectedRules: number) => + i18n.translate('xpack.siem.detectionEngine.rules.allRules.selectedRulesTitle', { + values: { selectedRules }, + defaultMessage: 'Selected {selectedRules} {selectedRules, plural, =1 {rule} other {rules}}', + }); + +export const EDIT_RULE_SETTINGS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.editRuleSettingsDescription', + { + defaultMessage: 'Edit rule settings', + } +); + +export const RUN_RULE_MANUALLY = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.runRuleManuallyDescription', + { + defaultMessage: 'Run rule manually…', + } +); + +export const DUPLICATE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.duplicateRuleDescription', + { + defaultMessage: 'Duplicate rule…', + } +); + +export const EXPORT_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.exportRuleDescription', + { + defaultMessage: 'Export rule', + } +); + +export const DELETE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.actions.deleteeRuleDescription', + { + defaultMessage: 'Delete rule…', + } +); + +export const COLUMN_RULE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.ruleTitle', + { + defaultMessage: 'Rule', + } +); + +export const COLUMN_METHOD = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.methodTitle', + { + defaultMessage: 'Method', + } +); + +export const COLUMN_SEVERITY = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.severityTitle', + { + defaultMessage: 'Severity', + } +); + +export const COLUMN_LAST_COMPLETE_RUN = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.lastCompletedRunTitle', + { + defaultMessage: 'Last completed run', + } +); + +export const COLUMN_LAST_RESPONSE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.lastResponseTitle', + { + defaultMessage: 'Last response', + } +); + +export const COLUMN_TAGS = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.tagsTitle', + { + defaultMessage: 'Tags', + } +); + +export const COLUMN_ACTIVATE = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.columns.activateTitle', + { + defaultMessage: 'Activate', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..8cbc61e677f8c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Rule } from '../../../containers/detection_engine/rules/types'; + +export interface EuiBasicTableSortTypes { + field: string; + direction: 'asc' | 'desc'; +} + +export interface EuiBasicTableOnChange { + page: { + index: number; + size: number; + }; + sort: EuiBasicTableSortTypes; +} + +export interface TableData { + id: string; + rule_id: string; + rule: { + href: string; + name: string; + status: string; + }; + method: string; + severity: string; + lastCompletedRun: string | undefined; + lastResponse: { + type: string; + message?: string; + }; + tags: string[]; + activate: boolean; + isLoading: boolean; + sourceRule: Rule; +} From 05f765a7a948bbd43ce3dad630cdaa148f888a74 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 21 Nov 2019 16:36:56 -0500 Subject: [PATCH 004/128] [Maps] Introduce fields (#50044) This introduces the `AbstractField` class and its implementations. Their use replace the ad-hoc object literals that were used to pass around field-level metadata in joins, metrics, styles, and tooltips. --- .../legacy/plugins/maps/common/constants.js | 13 ++ .../maps/public/actions/map_actions.js | 4 +- .../tooltip_selector.test.js.snap | 3 + .../public/components/tooltip_selector.js | 118 +++++++--- .../components/tooltip_selector.test.js | 40 +++- .../layer_panel/join_editor/resources/join.js | 9 +- .../features_tooltip/feature_properties.js | 1 + .../toc_entry/__snapshots__/view.test.js.snap | 6 +- .../layer_control/layer_toc/toc_entry/view.js | 2 +- .../layer_toc/toc_entry/view.test.js | 2 +- .../public/layers/fields/ems_file_field.js | 26 +++ .../maps/public/layers/fields/es_agg_field.js | 72 ++++++ .../maps/public/layers/fields/es_doc_field.js | 30 +++ .../maps/public/layers/fields/field.js | 45 ++++ .../layers/fields/kibana_region_field.js | 25 +++ .../maps/public/layers/heatmap_layer.js | 17 +- .../maps/public/layers/joins/inner_join.js | 29 ++- .../public/layers/joins/inner_join.test.js | 19 +- .../plugins/maps/public/layers/layer.js | 9 +- .../ems_file_source/ems_file_source.js | 50 +++-- .../ems_file_source/ems_file_source.test.js | 2 +- .../ems_file_source/update_source_editor.js | 14 +- .../public/layers/sources/es_agg_source.js | 149 ++++++------- .../es_geo_grid_source/es_geo_grid_source.js | 17 +- .../es_pew_pew_source/es_pew_pew_source.js | 15 +- .../update_source_editor.test.js.snap | 6 +- .../es_search_source/es_search_source.js | 61 ++--- .../es_search_source/update_source_editor.js | 25 ++- .../update_source_editor.test.js | 2 +- .../maps/public/layers/sources/es_source.js | 35 +-- .../public/layers/sources/es_term_source.js | 55 ++--- .../layers/sources/es_term_source.test.js | 35 ++- .../kibana_regionmap_source.js | 24 +- .../public/layers/sources/vector_source.js | 9 + .../components/legend/heatmap_legend.js | 72 ++++-- .../layers/styles/heatmap/heatmap_style.js | 4 +- .../legend/style_property_legend_row.js | 102 ++------- .../components/legend/vector_style_legend.js | 20 +- .../vector/components/style_option_shapes.js | 9 - .../vector/components/vector_style_editor.js | 27 ++- .../properties/dynamic_color_property.js | 23 +- .../dynamic_orientation_property.js | 8 + .../properties/dynamic_size_property.js | 67 +++++- .../properties/dynamic_style_property.js | 32 ++- .../properties/static_style_property.js | 4 +- .../vector/properties/style_property.js | 26 +++ .../layers/styles/vector/vector_style.js | 208 +++++++++--------- .../layers/styles/vector/vector_style.test.js | 94 +++++--- .../plugins/maps/public/layers/tile_layer.js | 4 - .../tooltips/es_aggmetric_tooltip_property.js | 5 +- .../layers/tooltips/join_tooltip_property.js | 14 +- .../maps/public/layers/vector_layer.js | 106 ++------- .../maps/public/layers/vector_tile_layer.js | 4 - .../maps/public/selectors/map_selectors.js | 31 +-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 56 files changed, 1116 insertions(+), 715 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/fields/ems_file_field.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/fields/field.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/fields/kibana_region_field.js diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index ade645d76cad9..691c679e5290b 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; export const EMS_CATALOGUE_PATH = 'ems/catalogue'; @@ -114,3 +115,15 @@ export const METRIC_TYPE = { SUM: 'sum', UNIQUE_COUNT: 'cardinality', }; + +export const COUNT_AGG_TYPE = METRIC_TYPE.COUNT; +export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', { + defaultMessage: 'count' +}); + +export const COUNT_PROP_NAME = 'doc_count'; + +export const STYLE_TYPE = { + 'STATIC': 'STATIC', + 'DYNAMIC': 'DYNAMIC' +}; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index 1f3dcabf6b205..324e975675454 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -735,9 +735,7 @@ export function clearMissingStyleProperties(layerId) { return; } - const dateFields = await targetLayer.getDateFields(); - const numberFields = await targetLayer.getNumberFields(); - const ordinalFields = [...dateFields, ...numberFields]; + const ordinalFields = await targetLayer.getOrdinalFields(); const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved(ordinalFields); if (hasChanges) { dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/tooltip_selector.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/tooltip_selector.test.js.snap index 97dd1f6566e07..4524f66c0642c 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/tooltip_selector.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/tooltip_selector.test.js.snap @@ -41,6 +41,7 @@ exports[`TooltipSelector should render component 1`] = ` "type": "string", }, Object { + "label": "foobar_label", "name": "iso3", "type": "string", }, @@ -50,7 +51,9 @@ exports[`TooltipSelector should render component 1`] = ` selectedFields={ Array [ Object { + "label": "foobar_label", "name": "iso2", + "type": "foobar_type", }, ] } diff --git a/x-pack/legacy/plugins/maps/public/components/tooltip_selector.js b/x-pack/legacy/plugins/maps/public/components/tooltip_selector.js index f5a4b94072a4d..50cbbdb3b7180 100644 --- a/x-pack/legacy/plugins/maps/public/components/tooltip_selector.js +++ b/x-pack/legacy/plugins/maps/public/components/tooltip_selector.js @@ -30,35 +30,109 @@ const reorder = (list, startIndex, endIndex) => { return result; }; +const getProps = async field => { + return new Promise(async (resolve, reject) => { + try { + const label = await field.getLabel(); + const type = await field.getDataType(); + resolve({ + label: label, + type: type, + name: field.getName() + }); + } catch(e) { + reject(e); + } + }); +}; + export class TooltipSelector extends Component { + state = { + fieldProps: [], + selectedFieldProps: [] + }; + + constructor() { + super(); + this._isMounted = false; + this._previousFields = null; + this._previousSelectedTooltips = null; + } + + componentDidMount() { + this._isMounted = true; + this._loadFieldProps(); + this._loadTooltipFieldProps(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + this._loadTooltipFieldProps(); + this._loadFieldProps(); + } + + async _loadTooltipFieldProps() { + + if (!this.props.tooltipFields || this.props.tooltipFields === this._previousSelectedTooltips) { + return; + } + + this._previousSelectedTooltips = this.props.tooltipFields; + const selectedProps = this.props.tooltipFields.map(getProps); + const selectedFieldProps = await Promise.all(selectedProps); + if (this._isMounted) { + this.setState({ selectedFieldProps }); + } + + } + + async _loadFieldProps() { + + if (!this.props.fields || this.props.fields === this._previousFields) { + return; + } + + this._previousFields = this.props.fields; + const props = this.props.fields.map(getProps); + const fieldProps = await Promise.all(props); + if (this._isMounted) { + this.setState({ fieldProps }); + } + + } + _getPropertyLabel = (propertyName) => { - if (!this.props.fields) { + if (!this.state.fieldProps.length) { return propertyName; } - - const field = this.props.fields.find(field => { + const prop = this.state.fieldProps.find((field) => { return field.name === propertyName; }); + return prop.label ? prop.label : propertyName; + } - return field && field.label - ? field.label - : propertyName; + _getTooltipProperties() { + return this.props.tooltipFields.map(field => field.getName()); } _onAdd = (properties) => { - if (!this.props.tooltipProperties) { + if (!this.props.tooltipFields) { this.props.onChange([...properties]); } else { - this.props.onChange([...this.props.tooltipProperties, ...properties]); + const existingProperties = this._getTooltipProperties(); + this.props.onChange([...existingProperties, ...properties]); } } _removeProperty = (index) => { - if (!this.props.tooltipProperties) { + if (!this.props.tooltipFields) { this.props.onChange([]); } else { - const tooltipProperties = [...this.props.tooltipProperties]; + const tooltipProperties = this._getTooltipProperties(); tooltipProperties.splice(index, 1); this.props.onChange(tooltipProperties); } @@ -70,11 +144,11 @@ export class TooltipSelector extends Component { return; } - this.props.onChange(reorder(this.props.tooltipProperties, source.index, destination.index)); + this.props.onChange(reorder(this._getTooltipProperties(), source.index, destination.index)); }; _renderProperties() { - if (!this.props.tooltipProperties) { + if (!this.state.selectedFieldProps.length) { return null; } @@ -82,12 +156,12 @@ export class TooltipSelector extends Component { {(provided, snapshot) => ( - this.props.tooltipProperties.map((propertyName, idx) => ( + this.state.selectedFieldProps.map((field, idx) => ( @@ -99,7 +173,7 @@ export class TooltipSelector extends Component { })} > - {this._getPropertyLabel(propertyName)} + {this._getPropertyLabel(field.name)}
    { - return { name: propertyName }; - }) - : []; - return (
    @@ -160,11 +227,12 @@ export class TooltipSelector extends Component {
    ); } } + diff --git a/x-pack/legacy/plugins/maps/public/components/tooltip_selector.test.js b/x-pack/legacy/plugins/maps/public/components/tooltip_selector.test.js index 10488640af99c..9797bce2cf8c9 100644 --- a/x-pack/legacy/plugins/maps/public/components/tooltip_selector.test.js +++ b/x-pack/legacy/plugins/maps/public/components/tooltip_selector.test.js @@ -9,33 +9,59 @@ import { shallow } from 'enzyme'; import { TooltipSelector } from './tooltip_selector'; + +class MockField { + constructor({ name, label, type }) { + this._name = name; + this._label = label; + this._type = type; + } + + getName() { + return this._name; + } + + async getLabel() { + return this._label || 'foobar_label'; + } + + async getDataType() { + return this._type || 'foobar_type'; + } +} + const defaultProps = { - tooltipProperties: ['iso2'], + tooltipFields: [new MockField({ name: 'iso2' })], onChange: (()=>{}), fields: [ - { + new MockField({ name: 'iso2', label: 'ISO 3166-1 alpha-2 code', type: 'string' - }, - { + }), + new MockField({ name: 'iso3', type: 'string' - }, + }) ] }; describe('TooltipSelector', () => { test('should render component', async () => { + const component = shallow( ); - expect(component) - .toMatchSnapshot(); + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js index 2b7c614f182fd..68d4656880666 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js @@ -111,7 +111,14 @@ export class Join extends Component { async _loadLeftFields() { let leftFields; try { - leftFields = await this.props.layer.getLeftJoinFields(); + const leftFieldsInstances = await this.props.layer.getLeftJoinFields(); + const leftFieldPromises = leftFieldsInstances.map(async (field) => { + return { + name: field.getName(), + label: await field.getLabel() + }; + }); + leftFields = await Promise.all(leftFieldPromises); } catch (error) { leftFields = []; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js index 866c099b841a1..7843770327011 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js @@ -155,6 +155,7 @@ export class FeatureProperties extends React.Component { } const rows = this.state.properties.map(tooltipProperty => { + const label = tooltipProperty.getPropertyName(); return ( diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap index 7fb7e58e81c99..2ca994647e1da 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap @@ -19,10 +19,10 @@ exports[`TOCEntry is rendered 1`] = ` Object { "getDisplayName": [Function], "getId": [Function], - "getLegendDetails": [Function], "hasErrors": [Function], "hasLegendDetails": [Function], "isVisible": [Function], + "renderLegendDetails": [Function], "showAtZoomLevel": [Function], } } @@ -87,10 +87,10 @@ exports[`TOCEntry props isReadOnly 1`] = ` Object { "getDisplayName": [Function], "getId": [Function], - "getLegendDetails": [Function], "hasErrors": [Function], "hasLegendDetails": [Function], "isVisible": [Function], + "renderLegendDetails": [Function], "showAtZoomLevel": [Function], } } @@ -137,10 +137,10 @@ exports[`TOCEntry props should display layer details when isLegendDetailsOpen is Object { "getDisplayName": [Function], "getId": [Function], - "getLegendDetails": [Function], "hasErrors": [Function], "hasLegendDetails": [Function], "isVisible": [Function], + "renderLegendDetails": [Function], "showAtZoomLevel": [Function], } } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index 8fd4ba8b13354..dc0756978010e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -227,7 +227,7 @@ export class TOCEntry extends React.Component { return null; } - const tocDetails = this.props.layer.getLegendDetails(); + const tocDetails = this.props.layer.renderLegendDetails(); if (!tocDetails) { return null; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js index dc0f9950919e4..ebb9fc27be149 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js @@ -13,7 +13,7 @@ const LAYER_ID = '1'; const mockLayer = { getId: () => { return LAYER_ID; }, - getLegendDetails: () => { return (
    TOC details mock
    ); }, + renderLegendDetails: () => { return (
    TOC details mock
    ); }, getDisplayName: () => { return 'layer 1'; }, isVisible: () => { return true; }, showAtZoomLevel: () => { return true; }, diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/ems_file_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/ems_file_field.js new file mode 100644 index 0000000000000..c6da7673ba606 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/ems_file_field.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { AbstractField } from './field'; +import { TooltipProperty } from '../tooltips/tooltip_property'; + +export class EMSFileField extends AbstractField { + static type = 'EMS_FILE'; + + async getLabel() { + const emsFileLayer = await this._source.getEMSFileLayer(); + const emsFields = emsFileLayer.getFieldsInLanguage(); + // Map EMS field name to language specific label + const emsField = emsFields.find(field => field.name === this.getName()); + return emsField ? emsField.description : this.getName(); + } + + async createTooltipProperty(value) { + const label = await this.getLabel(); + return new TooltipProperty(this.getName(), label, value); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js new file mode 100644 index 0000000000000..eb80169e94eab --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { AbstractField } from './field'; +import { COUNT_AGG_TYPE } from '../../../common/constants'; +import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property'; + +export class ESAggMetricField extends AbstractField { + + static type = 'ES_AGG'; + + constructor({ label, source, aggType, esDocField, origin }) { + super({ source, origin }); + this._label = label; + this._aggType = aggType; + this._esDocField = esDocField; + } + + getName() { + return this._source.formatMetricKey(this.getAggType(), this.getESDocFieldName()); + } + + async getLabel() { + return this._label ? await this._label : this._source.formatMetricLabel(this.getAggType(), this.getESDocFieldName()); + } + + getAggType() { + return this._aggType; + } + + isValid() { + return (this.getAggType() === COUNT_AGG_TYPE) ? true : !!this._esDocField; + } + + getESDocFieldName() { + return this._esDocField ? this._esDocField.getName() : ''; + } + + getRequestDescription() { + return this.getAggType() !== COUNT_AGG_TYPE ? `${this.getAggType()} ${this.getESDocFieldName()}` : COUNT_AGG_TYPE; + } + + async createTooltipProperty(value) { + const indexPattern = await this._source.getIndexPattern(); + return new ESAggMetricTooltipProperty( + this.getName(), + await this.getLabel(), + value, + indexPattern, + this + ); + } + + + makeMetricAggConfig() { + const metricAggConfig = { + id: this.getName(), + enabled: true, + type: this.getAggType(), + schema: 'metric', + params: {} + }; + if (this.getAggType() !== COUNT_AGG_TYPE) { + metricAggConfig.params = { field: this.getESDocFieldName() }; + } + return metricAggConfig; + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js new file mode 100644 index 0000000000000..5cc0c9a29ce02 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { AbstractField } from './field'; +import { ESTooltipProperty } from '../tooltips/es_tooltip_property'; + +export class ESDocField extends AbstractField { + + static type = 'ES_DOC'; + + async _getField() { + const indexPattern = await this._source.getIndexPattern(); + return indexPattern.fields.getByName(this._fieldName); + } + + async createTooltipProperty(value) { + const indexPattern = await this._source.getIndexPattern(); + return new ESTooltipProperty(this.getName(), this.getName(), value, indexPattern); + } + + async getDataType() { + const field = await this._getField(); + return field.type; + } + +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/field.js b/x-pack/legacy/plugins/maps/public/layers/fields/field.js new file mode 100644 index 0000000000000..b53c6991c6ebe --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/field.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { FIELD_ORIGIN } from '../../../common/constants'; + +export class AbstractField { + + constructor({ fieldName, source, origin }) { + this._fieldName = fieldName; + this._source = source; + this._origin = origin || FIELD_ORIGIN.SOURCE; + } + + getName() { + return this._fieldName; + } + + getSource() { + return this._source; + } + + isValid() { + return !!this._fieldName; + } + + async getDataType() { + return 'string'; + } + + async getLabel() { + return this._fieldName; + } + + async createTooltipProperty() { + throw new Error('must implement Field#createTooltipProperty'); + } + + getOrigin() { + return this._origin; + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/kibana_region_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/kibana_region_field.js new file mode 100644 index 0000000000000..248c34173a8c2 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/kibana_region_field.js @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import { AbstractField } from './field'; +import { TooltipProperty } from '../tooltips/tooltip_property'; + +export class KibanaRegionField extends AbstractField { + + static type = 'KIBANA_REGION'; + + async getLabel() { + const meta = await this._source.getVectorFileMeta(); + const field = meta.fields.find(f => f.name === this._fieldName); + return field ? field.description : this._fieldName; + } + + async createTooltipProperty(value) { + const label = await this.getLabel(); + return new TooltipProperty(this.getName(), label, value); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js index 10e528d19785b..0342975ce3192 100644 --- a/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/heatmap_layer.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { AbstractLayer } from './layer'; import { VectorLayer } from './vector_layer'; import { HeatmapStyle } from './styles/heatmap/heatmap_style'; @@ -23,17 +22,19 @@ export class HeatmapLayer extends VectorLayer { return heatmapLayerDescriptor; } - constructor({ layerDescriptor, source, style }) { - super({ layerDescriptor, source, style }); - if (!style) { + constructor({ layerDescriptor, source }) { + super({ layerDescriptor, source }); + if (!layerDescriptor.style) { const defaultStyle = HeatmapStyle.createDescriptor(); this._style = new HeatmapStyle(defaultStyle); + } else { + this._style = new HeatmapStyle(layerDescriptor.style); } } _getPropKeyOfSelectedMetric() { const metricfields = this._source.getMetricFields(); - return metricfields[0].propertyKey; + return metricfields[0].getName(); } _getHeatmapLayerId() { @@ -101,8 +102,8 @@ export class HeatmapLayer extends VectorLayer { return true; } - getLegendDetails() { - const label = _.get(this._source.getMetricFields(), '[0].propertyLabel', ''); - return this._style.getLegendDetails(label); + renderLegendDetails() { + const metricFields = this._source.getMetricFields(); + return this._style.renderLegendDetails(metricFields[0]); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js index c1ca1a90c15e6..184fdc0663bd7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js @@ -6,13 +6,15 @@ import { ESTermSource } from '../sources/es_term_source'; -import { VectorStyle } from '../styles/vector/vector_style'; +import { getComputedFieldNamePrefix } from '../styles/vector/style_util'; export class InnerJoin { - constructor(joinDescriptor, inspectorAdapters) { + constructor(joinDescriptor, leftSource) { this._descriptor = joinDescriptor; + const inspectorAdapters = leftSource.getInspectorAdapters(); this._rightSource = new ESTermSource(joinDescriptor.right, inspectorAdapters); + this._leftField = this._descriptor.leftField ? leftSource.createField({ fieldName: joinDescriptor.leftField }) : null; } destroy() { @@ -20,21 +22,15 @@ export class InnerJoin { } hasCompleteConfig() { - if (this._descriptor.leftField && this._rightSource) { + if (this._leftField && this._rightSource) { return this._rightSource.hasCompleteConfig(); } return false; } - getRightMetricFields() { - return this._rightSource.getMetricFields(); - } - getJoinFields() { - return this.getRightMetricFields().map(({ propertyKey: name, propertyLabel: label }) => { - return { label, name }; - }); + return this._rightSource.getMetricFields(); } // Source request id must be static and unique because the re-fetch logic uses the id to locate the previous request. @@ -44,18 +40,19 @@ export class InnerJoin { return `join_source_${this._rightSource.getId()}`; } - getLeftFieldName() { - return this._descriptor.leftField; + getLeftField() { + return this._leftField; } - joinPropertiesToFeature(feature, propertiesMap, rightMetricFields) { + joinPropertiesToFeature(feature, propertiesMap) { + const rightMetricFields = this._rightSource.getMetricFields(); // delete feature properties added by previous join for (let j = 0; j < rightMetricFields.length; j++) { - const { propertyKey: metricPropertyKey } = rightMetricFields[j]; + const metricPropertyKey = rightMetricFields[j].getName(); delete feature.properties[metricPropertyKey]; // delete all dynamic properties for metric field - const stylePropertyPrefix = VectorStyle.getComputedFieldNamePrefix(metricPropertyKey); + const stylePropertyPrefix = getComputedFieldNamePrefix(metricPropertyKey); Object.keys(feature.properties).forEach(featurePropertyKey => { if (featurePropertyKey.length >= stylePropertyPrefix.length && featurePropertyKey.substring(0, stylePropertyPrefix.length) === stylePropertyPrefix) { @@ -64,7 +61,7 @@ export class InnerJoin { }); } - const joinKey = feature.properties[this._descriptor.leftField]; + const joinKey = feature.properties[this._leftField.getName()]; const coercedKey = typeof joinKey === 'undefined' || joinKey === null ? null : joinKey.toString(); if (propertiesMap && coercedKey !== null && propertiesMap.has(coercedKey)) { Object.assign(feature.properties, propertiesMap.get(coercedKey)); diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js index c493062723470..02410c64c1c42 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js @@ -23,12 +23,25 @@ const rightSource = { indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247', indexPatternTitle: 'kibana_sample_data_logs', term: 'geo.dest', + metrics: [{ type: 'count' }] +}; + +const mockSource = { + getInspectorAdapters() { + }, + createField({ fieldName: name }) { + return { + getName() { + return name; + } + }; + } }; const leftJoin = new InnerJoin({ leftField: 'iso2', right: rightSource -}); +}, mockSource); const COUNT_PROPERTY_NAME = '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.dest'; describe('joinPropertiesToFeature', () => { @@ -76,7 +89,7 @@ describe('joinPropertiesToFeature', () => { const leftJoin = new InnerJoin({ leftField: 'zipcode', right: rightSource - }); + }, mockSource); const feature = { properties: { @@ -118,7 +131,7 @@ describe('joinPropertiesToFeature', () => { const leftJoin = new InnerJoin({ leftField: 'code', right: rightSource - }); + }, mockSource); const feature = { properties: { diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 3cde1b4bc0a41..72a89046ed2f5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -24,10 +24,9 @@ const NO_SOURCE_UPDATE_REQUIRED = false; export class AbstractLayer { - constructor({ layerDescriptor, source, style }) { + constructor({ layerDescriptor, source }) { this._descriptor = AbstractLayer.createDescriptor(layerDescriptor); this._source = source; - this._style = style; if (this._descriptor.__dataRequests) { this._dataRequests = this._descriptor.__dataRequests.map(dataRequest => new DataRequest(dataRequest)); } else { @@ -196,7 +195,7 @@ export class AbstractLayer { return false; } - getLegendDetails() { + renderLegendDetails() { return null; } @@ -395,6 +394,10 @@ export class AbstractLayer { return []; } + async getOrdinalFields() { + return []; + } + syncVisibilityWithMb(mbMap, mbLayerId) { mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index 3af1378e8e016..b2e04f56e5718 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -7,13 +7,13 @@ import { AbstractVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; -import { EMS_FILE, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { EMS_FILE, FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants'; import { getEMSClient } from '../../../meta'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { UpdateSourceEditor } from './update_source_editor'; -import { TooltipProperty } from '../../tooltips/tooltip_property'; +import { EMSFileField } from '../../fields/ems_file_field'; export class EMSFileSource extends AbstractVectorSource { @@ -45,19 +45,29 @@ export class EMSFileSource extends AbstractVectorSource { constructor(descriptor, inspectorAdapters) { super(EMSFileSource.createDescriptor(descriptor), inspectorAdapters); + this._tooltipFields = this._descriptor.tooltipProperties.map(propertyKey => this.createField({ fieldName: propertyKey })); + } + + createField({ fieldName }) { + return new EMSFileField({ + fieldName, + source: this, + origin: FIELD_ORIGIN.SOURCE + }); } renderSourceSettingsEditor({ onChange }) { return ( ); } - async _getEMSFileLayer() { + async getEMSFileLayer() { const emsClient = getEMSClient(); const emsFileLayers = await emsClient.getFileLayers(); const emsFileLayer = emsFileLayers.find((fileLayer => fileLayer.getId() === this._descriptor.id)); @@ -73,7 +83,7 @@ export class EMSFileSource extends AbstractVectorSource { } async getGeoJsonWithMeta() { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); const featureCollection = await AbstractVectorSource.getGeoJson({ format: emsFileLayer.getDefaultFormatType(), featureCollectionPath: 'data', @@ -98,7 +108,7 @@ export class EMSFileSource extends AbstractVectorSource { async getImmutableProperties() { let emsLink; try { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); emsLink = emsFileLayer.getEMSHotLink(); } catch(error) { // ignore error if EMS layer id could not be found @@ -121,7 +131,7 @@ export class EMSFileSource extends AbstractVectorSource { async getDisplayName() { try { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); return emsFileLayer.getDisplayName(); } catch (error) { return this._descriptor.id; @@ -129,36 +139,28 @@ export class EMSFileSource extends AbstractVectorSource { } async getAttributions() { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); return emsFileLayer.getAttributions(); } async getLeftJoinFields() { - const emsFileLayer = await this._getEMSFileLayer(); + const emsFileLayer = await this.getEMSFileLayer(); const fields = emsFileLayer.getFieldsInLanguage(); - return fields.map(f => { - return { name: f.name, label: f.description }; - }); + return fields.map(f => this.createField({ fieldName: f.name })); } canFormatFeatureProperties() { - return this._descriptor.tooltipProperties.length; + return this._tooltipFields.length > 0; } async filterAndFormatPropertiesToHtml(properties) { - const emsFileLayer = await this._getEMSFileLayer(); - const emsFields = emsFileLayer.getFieldsInLanguage(); - - return this._descriptor.tooltipProperties.map(propertyName => { - // Map EMS field name to language specific label - const emsField = emsFields.find(field => { - return field.name === propertyName; - }); - const label = emsField ? emsField.description : propertyName; - - return new TooltipProperty(propertyName, label, properties[propertyName]); + const tooltipProperties = this._tooltipFields.map(field => { + const value = properties[field.getName()]; + return field.createTooltipProperty(value); }); + + return Promise.all(tooltipProperties); } async getSupportedShapeTypes() { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js index d9f759bdcc2cd..15581f1cfbacb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.js @@ -13,7 +13,7 @@ function makeEMSFileSource(tooltipProperties) { const emsFileSource = new EMSFileSource({ tooltipProperties: tooltipProperties }); - emsFileSource._getEMSFileLayer = () => { + emsFileSource.getEMSFileLayer = () => { return { getFieldsInLanguage() { return [{ diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js index ccd7592649e21..f901c8b93e8cd 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js @@ -13,7 +13,8 @@ export class UpdateSourceEditor extends Component { static propTypes = { onChange: PropTypes.func.isRequired, - tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired + tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired, + source: PropTypes.object }; state = { @@ -36,16 +37,12 @@ export class UpdateSourceEditor extends Component { const emsFiles = await emsClient.getFileLayers(); const emsFile = emsFiles.find((emsFile => emsFile.getId() === this.props.layerId)); const emsFields = emsFile.getFieldsInLanguage(); - fields = emsFields.map(field => { - return { - name: field.name, - label: field.description - }; - }); + fields = emsFields.map(field => this.props.source.createField({ fieldName: field.name })); } catch(e) { //swallow this error. when a matching EMS-config cannot be found, the source already will have thrown errors during the data request. This will propagate to the vector-layer and be displayed in the UX fields = []; } + if (this._isMounted) { this.setState({ fields: fields }); } @@ -56,9 +53,10 @@ export class UpdateSourceEditor extends Component { }; render() { + return ( diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js index d9639144dfc52..fc28f4cf3a900 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js @@ -5,12 +5,10 @@ */ import { AbstractESSource } from './es_source'; -import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property'; -import { METRIC_TYPE } from '../../../common/constants'; -import _ from 'lodash'; +import { ESAggMetricField } from '../fields/es_agg_field'; +import { ESDocField } from '../fields/es_doc_field'; +import { METRIC_TYPE, COUNT_AGG_TYPE, COUNT_PROP_LABEL, COUNT_PROP_NAME, FIELD_ORIGIN } from '../../../common/constants'; -const COUNT_PROP_LABEL = 'count'; -const COUNT_PROP_NAME = 'doc_count'; const AGG_DELIMITER = '_of_'; export class AbstractESAggSource extends AbstractESSource { @@ -34,109 +32,106 @@ export class AbstractESAggSource extends AbstractESSource { ] }; - _formatMetricKey(metric) { - const aggType = metric.type; - const fieldName = metric.field; - return aggType !== METRIC_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME; + constructor(descriptor, inspectorAdapters) { + super(descriptor, inspectorAdapters); + this._metricFields = this._descriptor.metrics ? this._descriptor.metrics.map(metric => { + const esDocField = metric.field ? new ESDocField({ fieldName: metric.field, source: this }) : null; + return new ESAggMetricField({ + label: metric.label, + esDocField: esDocField, + aggType: metric.type, + source: this, + origin: this.getOriginForField() + }); + }) : []; } - _formatMetricLabel(metric) { - const aggType = metric.type; - const fieldName = metric.field; - return aggType !== METRIC_TYPE.COUNT ? `${aggType} of ${fieldName}` : COUNT_PROP_LABEL; - } + createField({ fieldName, label }) { - _getValidMetrics() { - const metrics = _.get(this._descriptor, 'metrics', []).filter(({ type, field }) => { - if (type === METRIC_TYPE.COUNT) { - return true; + //if there is a corresponding field with a custom label, use that one. + if (!label) { + const matchField = this._metricFields.find(field => field.getName() === fieldName); + if (matchField) { + label = matchField.getLabel(); } + } - if (field) { - return true; - } - return false; + if (fieldName === COUNT_PROP_NAME) { + return new ESAggMetricField({ + aggType: COUNT_AGG_TYPE, + label: label, + source: this, + origin: this.getOriginForField() + }); + } + //this only works because aggType is a fixed set and does not include the `_of_` string + const [aggType, docField] = fieldName.split(AGG_DELIMITER); + const esDocField = new ESDocField({ fieldName: docField, source: this }); + return new ESAggMetricField({ + label: label, + esDocField, + aggType, + source: this, + origin: this.getOriginForField() + }); + } + + getMetricFieldForName(fieldName) { + return this.getMetricFields().find(metricField => { + return metricField.getName() === fieldName; }); + } + + getOriginForField() { + return FIELD_ORIGIN.SOURCE; + } + + getMetricFields() { + const metrics = this._metricFields.filter(esAggField => esAggField.isValid()); if (metrics.length === 0) { - metrics.push({ type: METRIC_TYPE.COUNT }); + metrics.push(new ESAggMetricField({ + aggType: COUNT_AGG_TYPE, + source: this, + origin: this.getOriginForField() + })); } return metrics; } - getMetricFields() { - return this._getValidMetrics().map(metric => { - const metricKey = this._formatMetricKey(metric); - const metricLabel = metric.label ? metric.label : this._formatMetricLabel(metric); - const metricCopy = { ...metric }; - delete metricCopy.label; - return { - ...metricCopy, - propertyKey: metricKey, - propertyLabel: metricLabel - }; - }); + formatMetricKey(aggType, fieldName) { + return aggType !== COUNT_AGG_TYPE ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME; } - async getNumberFields() { - return this.getMetricFields().map(({ propertyKey: name, propertyLabel: label }) => { - return { label, name }; - }); + formatMetricLabel(aggType, fieldName) { + return aggType !== COUNT_AGG_TYPE ? `${aggType} of ${fieldName}` : COUNT_PROP_LABEL; } - getFieldNames() { - return this.getMetricFields().map(({ propertyKey }) => { - return propertyKey; - }); + createMetricAggConfigs() { + return this.getMetricFields().map(esAggMetric => esAggMetric.makeMetricAggConfig()); } - createMetricAggConfigs() { - return this.getMetricFields().map(metric => { - const metricAggConfig = { - id: metric.propertyKey, - enabled: true, - type: metric.type, - schema: 'metric', - params: {} - }; - if (metric.type !== METRIC_TYPE.COUNT) { - metricAggConfig.params = { field: metric.field }; - } - return metricAggConfig; - }); + + async getNumberFields() { + return this.getMetricFields(); } async filterAndFormatPropertiesToHtmlForMetricFields(properties) { - let indexPattern; - try { - indexPattern = await this._getIndexPattern(); - } catch(error) { - console.warn(`Unable to find Index pattern ${this._descriptor.indexPatternId}, values are not formatted`); - return properties; - } const metricFields = this.getMetricFields(); - const tooltipProperties = []; + const tooltipPropertiesPromises = []; metricFields.forEach((metricField) => { let value; for (const key in properties) { - if (properties.hasOwnProperty(key) && metricField.propertyKey === key) { + if (properties.hasOwnProperty(key) && metricField.getName() === key) { value = properties[key]; break; } } - const tooltipProperty = new ESAggMetricTooltipProperty( - metricField.propertyKey, - metricField.propertyLabel, - value, - indexPattern, - metricField - ); - tooltipProperties.push(tooltipProperty); + const tooltipPromise = metricField.createTooltipProperty(value); + tooltipPropertiesPromises.push(tooltipPromise); }); - return tooltipProperties; - + return await Promise.all(tooltipPropertiesPromises); } - } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 53a77dc0c00a8..413f99480a8c2 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -20,13 +20,12 @@ import { RENDER_AS } from './render_as'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { GRID_RESOLUTION } from '../../grid_resolution'; -import { SOURCE_DATA_ID_ORIGIN, ES_GEO_GRID } from '../../../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, ES_GEO_GRID, COUNT_PROP_LABEL, COUNT_PROP_NAME } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { AbstractESAggSource } from '../es_agg_source'; +import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -const COUNT_PROP_LABEL = 'count'; -const COUNT_PROP_NAME = 'doc_count'; const MAX_GEOTILE_LEVEL = 29; const aggSchemas = new Schemas([ @@ -93,7 +92,7 @@ export class ESGeoGridSource extends AbstractESAggSource { async getImmutableProperties() { let indexPatternTitle = this._descriptor.indexPatternId; try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); indexPatternTitle = indexPattern.title; } catch (error) { // ignore error, title will just default to id @@ -124,6 +123,10 @@ export class ESGeoGridSource extends AbstractESAggSource { ]; } + getFieldNames() { + return this.getMetricFields().map((esAggMetricField => esAggMetricField.getName())); + } + isGeoGridPrecisionAware() { return true; } @@ -163,7 +166,7 @@ export class ESGeoGridSource extends AbstractESAggSource { } async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const searchSource = await this._makeSearchSource(searchFilters, 0); const aggConfigs = new AggConfigs(indexPattern, this._makeAggConfigs(searchFilters.geogridPrecision), aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); @@ -225,7 +228,7 @@ export class ESGeoGridSource extends AbstractESAggSource { }); descriptor.style = VectorStyle.createDescriptor({ [vectorStyles.FILL_COLOR]: { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + type: DynamicStyleProperty.type, options: { field: { label: COUNT_PROP_LABEL, @@ -236,7 +239,7 @@ export class ESGeoGridSource extends AbstractESAggSource { } }, [vectorStyles.ICON_SIZE]: { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + type: DynamicStyleProperty.type, options: { field: { label: COUNT_PROP_LABEL, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 0e224578c5754..01220136b14f3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -14,15 +14,14 @@ import { UpdateSourceEditor } from './update_source_editor'; import { VectorStyle } from '../../styles/vector/vector_style'; import { vectorStyles } from '../../styles/vector/vector_style_defaults'; import { i18n } from '@kbn/i18n'; -import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW } from '../../../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW, COUNT_PROP_NAME, COUNT_PROP_LABEL } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggConfigs } from 'ui/agg_types'; import { AbstractESAggSource } from '../es_agg_source'; +import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -const COUNT_PROP_LABEL = 'count'; -const COUNT_PROP_NAME = 'doc_count'; const MAX_GEOTILE_LEVEL = 29; const aggSchemas = new Schemas([AbstractESAggSource.METRIC_SCHEMA_CONFIG]); @@ -92,7 +91,7 @@ export class ESPewPewSource extends AbstractESAggSource { async getImmutableProperties() { let indexPatternTitle = this._descriptor.indexPatternId; try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); indexPatternTitle = indexPattern.title; } catch (error) { // ignore error, title will just default to id @@ -126,7 +125,7 @@ export class ESPewPewSource extends AbstractESAggSource { createDefaultLayer(options) { const styleDescriptor = VectorStyle.createDescriptor({ [vectorStyles.LINE_COLOR]: { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + type: DynamicStyleProperty.type, options: { field: { label: COUNT_PROP_LABEL, @@ -137,7 +136,7 @@ export class ESPewPewSource extends AbstractESAggSource { } }, [vectorStyles.LINE_WIDTH]: { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + type: DynamicStyleProperty.type, options: { field: { label: COUNT_PROP_LABEL, @@ -167,7 +166,7 @@ export class ESPewPewSource extends AbstractESAggSource { } async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const metricAggConfigs = this.createMetricAggConfigs(); const aggConfigs = new AggConfigs(indexPattern, metricAggConfigs, aggSchemas.all); @@ -223,7 +222,7 @@ export class ESPewPewSource extends AbstractESAggSource { } async _getGeoField() { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const geoField = indexPattern.fields.getByName(this._descriptor.destGeoField); if (!geoField) { throw new Error(i18n.translate('xpack.maps.source.esSource.noGeoFieldErrorMessage', { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap index eb9bba76e4405..1e064fdb0dd7d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap @@ -13,7 +13,7 @@ exports[`should enable sort order select when sort field provided 1`] = ` this.createField({ fieldName: property })); + } + + createField({ fieldName }) { + return new ESDocField({ + fieldName, + source: this + }); } renderSourceSettingsEditor({ onChange }) { return ( { - return { name: field.name, label: field.name }; + return this.createField({ fieldName: field.name }); }); } catch (error) { return []; @@ -100,19 +110,15 @@ export class ESSearchSource extends AbstractESSource { async getDateFields() { try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); return indexPattern.fields.getByType('date').map(field => { - return { name: field.name, label: field.name }; + return this.createField({ fieldName: field.name }); }); } catch (error) { return []; } } - getMetricFields() { - return []; - } - getFieldNames() { return [this._descriptor.geoField]; } @@ -121,7 +127,7 @@ export class ESSearchSource extends AbstractESSource { let indexPatternTitle = this._descriptor.indexPatternId; let geoFieldType = ''; try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); indexPatternTitle = indexPattern.title; const geoField = await this._getGeoField(); geoFieldType = geoField.type; @@ -171,7 +177,7 @@ export class ESSearchSource extends AbstractESSource { } async _excludeDateFields(fieldNames) { - const dateFieldNames = _.map(await this.getDateFields(), 'name'); + const dateFieldNames = (await this.getDateFields()).map(field => field.getName()); return fieldNames.filter(field => { return !dateFieldNames.includes(field); }); @@ -179,7 +185,7 @@ export class ESSearchSource extends AbstractESSource { // Returns docvalue_fields array for the union of indexPattern's dateFields and request's field names. async _getDateDocvalueFields(searchFields) { - const dateFieldNames = _.map(await this.getDateFields(), 'name'); + const dateFieldNames = (await this.getDateFields()).map(field => field.getName()); return searchFields .filter(fieldName => { return dateFieldNames.includes(fieldName); @@ -198,7 +204,7 @@ export class ESSearchSource extends AbstractESSource { topHitsSize, } = this._descriptor; - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const geoField = await this._getGeoField(); const scriptFields = {}; @@ -329,7 +335,7 @@ export class ESSearchSource extends AbstractESSource { ? await this._getTopHits(layerName, searchFilters, registerCancelCallback) : await this._getSearchHits(layerName, searchFilters, registerCancelCallback); - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const unusedMetaFields = indexPattern.metaFields.filter(metaField => { return !['_id', '_index'].includes(metaField); }); @@ -362,11 +368,11 @@ export class ESSearchSource extends AbstractESSource { } canFormatFeatureProperties() { - return this._descriptor.tooltipProperties.length > 0; + return this._tooltipFields.length > 0; } async _loadTooltipProperties(docId, index, indexPattern) { - if (this._descriptor.tooltipProperties.length === 0) { + if (this._tooltipFields.length === 0) { return {}; } @@ -378,7 +384,7 @@ export class ESSearchSource extends AbstractESSource { query: `_id:"${docId}" and _index:${index}` }; searchSource.setField('query', query); - searchSource.setField('fields', this._descriptor.tooltipProperties); + searchSource.setField('fields', this._getTooltipPropertyNames()); const resp = await searchSource.fetch(); @@ -394,7 +400,7 @@ export class ESSearchSource extends AbstractESSource { const properties = indexPattern.flattenHit(hit); indexPattern.metaFields.forEach(metaField => { - if (!this._descriptor.tooltipProperties.includes(metaField)) { + if (!this._getTooltipPropertyNames().includes(metaField)) { delete properties[metaField]; } }); @@ -402,12 +408,13 @@ export class ESSearchSource extends AbstractESSource { } async filterAndFormatPropertiesToHtml(properties) { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const propertyValues = await this._loadTooltipProperties(properties._id, properties._index, indexPattern); - - return this._descriptor.tooltipProperties.map(propertyName => { - return new ESTooltipProperty(propertyName, propertyName, propertyValues[propertyName], indexPattern); + const tooltipProperties = this._tooltipFields.map(field => { + const value = propertyValues[field.getName()]; + return field.createTooltipProperty(value); }); + return Promise.all(tooltipProperties); } isFilterByMapBounds() { @@ -415,12 +422,9 @@ export class ESSearchSource extends AbstractESSource { } async getLeftJoinFields() { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); // Left fields are retrieved from _source. - return getSourceFields(indexPattern.fields) - .map(field => { - return { name: field.name, label: field.name }; - }); + return getSourceFields(indexPattern.fields).map(field => this.createField({ fieldName: field.name })); } async getSupportedShapeTypes() { @@ -507,7 +511,6 @@ export class ESSearchSource extends AbstractESSource { async getPreIndexedShape(properties) { const geoField = await this._getGeoField(); - return { index: properties._index, // Can not use index pattern title because it may reference many indices id: properties._id, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js index 760e087f2e6f6..1b4e999c29d0a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js @@ -22,22 +22,24 @@ import { i18n } from '@kbn/i18n'; import { getTermsFields, getSourceFields } from '../../../index_pattern_util'; import { ValidatedRange } from '../../../components/validated_range'; import { SORT_ORDER } from '../../../../common/constants'; +import { ESDocField } from '../../fields/es_doc_field'; export class UpdateSourceEditor extends Component { static propTypes = { indexPatternId: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, filterByMapBounds: PropTypes.bool.isRequired, - tooltipProperties: PropTypes.arrayOf(PropTypes.string).isRequired, + tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired, sortField: PropTypes.string, sortOrder: PropTypes.string.isRequired, useTopHits: PropTypes.bool.isRequired, topHitsSplitField: PropTypes.string, topHitsSize: PropTypes.number.isRequired, + source: PropTypes.object }; state = { - tooltipFields: null, + sourceFields: null, termFields: null, sortFields: null, }; @@ -73,10 +75,19 @@ export class UpdateSourceEditor extends Component { return; } + //todo move this all to the source + const rawTooltipFields = getSourceFields(indexPattern.fields); + const sourceFields = rawTooltipFields.map(field => { + return new ESDocField({ + fieldName: field.name, + source: this.props.source + }); + }); + this.setState({ - tooltipFields: getSourceFields(indexPattern.fields), - termFields: getTermsFields(indexPattern.fields), - sortFields: indexPattern.fields.filter(field => field.sortable), + sourceFields: sourceFields, + termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields + sortFields: indexPattern.fields.filter(field => field.sortable), //todo change sort fields to use fields }); } _onTooltipPropertiesChange = propertyNames => { @@ -173,9 +184,9 @@ export class UpdateSourceEditor extends Component { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js index 9a3a74e0ed680..5a1b83589a1ee 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js @@ -15,7 +15,7 @@ const defaultProps = { indexPatternId: 'indexPattern1', onChange: () => {}, filterByMapBounds: true, - tooltipProperties: [], + tooltipFields: [], sortOrder: 'DESC', useTopHits: false, topHitsSplitField: 'trackId', diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 010b9360d6501..c2f4f7e755288 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -69,6 +69,10 @@ export class AbstractESSource extends AbstractVectorSource { return clonedDescriptor; } + getMetricFields() { + return []; + } + async _runEsQuery(requestName, searchSource, registerCancelCallback, requestDescription) { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); @@ -95,7 +99,7 @@ export class AbstractESSource extends AbstractVectorSource { } async _makeSearchSource(searchFilters, limit, initialSearchContext) { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const isTimeAware = await this.isTimeAware(); const applyGlobalQuery = _.get(searchFilters, 'applyGlobalQuery', true); const globalFilters = applyGlobalQuery ? searchFilters.filters : []; @@ -130,7 +134,7 @@ export class AbstractESSource extends AbstractVectorSource { const searchSource = await this._makeSearchSource({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }, 0); const geoField = await this._getGeoField(); - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const geoBoundsAgg = [{ type: 'geo_bounds', @@ -171,7 +175,7 @@ export class AbstractESSource extends AbstractVectorSource { async isTimeAware() { try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const timeField = indexPattern.timeFieldName; return !!timeField; } catch (error) { @@ -179,7 +183,7 @@ export class AbstractESSource extends AbstractVectorSource { } } - async _getIndexPattern() { + async getIndexPattern() { if (this.indexPattern) { return this.indexPattern; } @@ -208,7 +212,7 @@ export class AbstractESSource extends AbstractVectorSource { async _getGeoField() { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const geoField = indexPattern.fields.getByName(this._descriptor.geoField); if (!geoField) { throw new Error(i18n.translate('xpack.maps.source.esSource.noGeoFieldErrorMessage', { @@ -221,7 +225,7 @@ export class AbstractESSource extends AbstractVectorSource { async getDisplayName() { try { - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); return indexPattern.title; } catch (error) { // Unable to load index pattern, just return id as display name @@ -238,25 +242,27 @@ export class AbstractESSource extends AbstractVectorSource { } async getFieldFormatter(fieldName) { - const metricField = this.getMetricFields().find(({ propertyKey }) => { - return propertyKey === fieldName; - }); + + const metricField = this.getMetricFields().find(field => field.getName() === fieldName); // Do not use field formatters for counting metrics - if (metricField && metricField.type === METRIC_TYPE.COUNT || metricField.type === METRIC_TYPE.UNIQUE_COUNT) { + if (metricField && (metricField.type === METRIC_TYPE.COUNT || metricField.type === METRIC_TYPE.UNIQUE_COUNT)) { + return null; + } + + // fieldName could be an aggregation so it needs to be unpacked to expose raw field. + const realFieldName = metricField ? metricField.getESDocFieldName() : fieldName; + if (!realFieldName) { return null; } let indexPattern; try { - indexPattern = await this._getIndexPattern(); + indexPattern = await this.getIndexPattern(); } catch(error) { return null; } - const realFieldName = metricField - ? metricField.field - : fieldName; const fieldFromIndexPattern = indexPattern.fields.getByName(realFieldName); if (!fieldFromIndexPattern) { return null; @@ -264,4 +270,5 @@ export class AbstractESSource extends AbstractVectorSource { return fieldFromIndexPattern.format.getConverterFor('text'); } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index 7d1ccf7373cf6..afc402fa81bcb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -9,12 +9,15 @@ import _ from 'lodash'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggConfigs } from 'ui/agg_types'; import { i18n } from '@kbn/i18n'; -import { ESTooltipProperty } from '../tooltips/es_tooltip_property'; -import { ES_SIZE_LIMIT, METRIC_TYPE } from '../../../common/constants'; +import { ES_SIZE_LIMIT, FIELD_ORIGIN, METRIC_TYPE } from '../../../common/constants'; +import { ESDocField } from '../fields/es_doc_field'; import { AbstractESAggSource } from './es_agg_source'; const TERMS_AGG_NAME = 'join'; +const FIELD_NAME_PREFIX = '__kbnjoin__'; +const GROUP_BY_DELIMITER = '_groupby_'; + const aggSchemas = new Schemas([ AbstractESAggSource.METRIC_SCHEMA_CONFIG, { @@ -48,6 +51,10 @@ export class ESTermSource extends AbstractESAggSource { static type = 'ES_TERM_SOURCE'; + constructor(descriptor, inspectorAdapters) { + super(descriptor, inspectorAdapters); + this._termField = new ESDocField({ fieldName: descriptor.term, source: this, origin: this.getOriginForField() }); + } static renderEditor({}) { //no need to localize. this editor is never rendered. @@ -62,22 +69,26 @@ export class ESTermSource extends AbstractESAggSource { return [this._descriptor.indexPatternId]; } - getTerm() { - return this._descriptor.term; + getTermField() { + return this._termField; + } + + getOriginForField() { + return FIELD_ORIGIN.JOIN; } getWhereQuery() { return this._descriptor.whereQuery; } - _formatMetricKey(metric) { - const metricKey = metric.type !== METRIC_TYPE.COUNT ? `${metric.type}_of_${metric.field}` : metric.type; - return `__kbnjoin__${metricKey}_groupby_${this._descriptor.indexPatternTitle}.${this._descriptor.term}`; + formatMetricKey(aggType, fieldName) { + const metricKey = aggType !== METRIC_TYPE.COUNT ? `${aggType}_of_${fieldName}` : aggType; + return `${FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${this._descriptor.indexPatternTitle}.${this._termField.getName()}`; } - _formatMetricLabel(metric) { - const metricLabel = metric.type !== METRIC_TYPE.COUNT ? `${metric.type} ${metric.field}` : 'count'; - return `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._descriptor.term}`; + formatMetricLabel(type, fieldName) { + const metricLabel = type !== METRIC_TYPE.COUNT ? `${type} ${fieldName}` : 'count'; + return `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._termField.getName()}`; } async getPropertiesMap(searchFilters, leftSourceName, leftFieldName, registerCancelCallback) { @@ -86,13 +97,13 @@ export class ESTermSource extends AbstractESAggSource { return []; } - const indexPattern = await this._getIndexPattern(); + const indexPattern = await this.getIndexPattern(); const searchSource = await this._makeSearchSource(searchFilters, 0); const configStates = this._makeAggConfigs(); const aggConfigs = new AggConfigs(indexPattern, configStates, aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const requestName = `${this._descriptor.indexPatternTitle}.${this._descriptor.term}`; + const requestName = `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`; const requestDesc = this._getRequestDescription(leftSourceName, leftFieldName); const rawEsData = await this._runEsQuery(requestName, searchSource, registerCancelCallback, requestDesc); @@ -117,15 +128,13 @@ export class ESTermSource extends AbstractESAggSource { } _getRequestDescription(leftSourceName, leftFieldName) { - const metrics = this._getValidMetrics().map(metric => { - return metric.type !== METRIC_TYPE.COUNT ? `${metric.type} ${metric.field}` : 'count'; - }); + const metrics = this.getMetricFields().map(esAggMetric => esAggMetric.getRequestDescription()); const joinStatement = []; joinStatement.push(i18n.translate('xpack.maps.source.esJoin.joinLeftDescription', { defaultMessage: `Join {leftSourceName}:{leftFieldName} with`, values: { leftSourceName, leftFieldName } })); - joinStatement.push(`${this._descriptor.indexPatternTitle}:${this._descriptor.term}`); + joinStatement.push(`${this._descriptor.indexPatternTitle}:${this._termField.getName()}`); joinStatement.push(i18n.translate('xpack.maps.source.esJoin.joinMetricsDescription', { defaultMessage: `for metrics {metrics}`, values: { metrics: metrics.join(',') } @@ -148,7 +157,7 @@ export class ESTermSource extends AbstractESAggSource { type: 'terms', schema: 'segment', params: { - field: this._descriptor.term, + field: this._termField.getName(), size: ES_SIZE_LIMIT } } @@ -164,15 +173,7 @@ export class ESTermSource extends AbstractESAggSource { return await this.filterAndFormatPropertiesToHtmlForMetricFields(properties); } - async createESTooltipProperty(propertyName, rawValue) { - try { - const indexPattern = await this._getIndexPattern(); - if (!indexPattern) { - return null; - } - return new ESTooltipProperty(propertyName, propertyName, rawValue, indexPattern); - } catch (e) { - return null; - } + getFieldNames() { + return this.getMetricFields().map(esAggMetricField => esAggMetricField.getName()); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js index ea11c7e367e5b..7f6415fcfae85 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js @@ -36,21 +36,21 @@ const metricExamples = [ describe('getMetricFields', () => { - it('should add default "count" metric when no metrics are provided', () => { + it('should add default "count" metric when no metrics are provided', async () => { const source = new ESTermSource({ indexPatternTitle: indexPatternTitle, term: termFieldName, }); const metrics = source.getMetricFields(); expect(metrics.length).toBe(1); - expect(metrics[0]).toEqual({ - type: 'count', - propertyKey: '__kbnjoin__count_groupby_myIndex.myTermField', - propertyLabel: 'count of myIndex:myTermField', - }); + + expect(metrics[0].getAggType()).toEqual('count'); + expect(metrics[0].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField'); + expect(await metrics[0].getLabel()).toEqual('count of myIndex:myTermField'); + }); - it('should remove incomplete metric configurations', () => { + it('should remove incomplete metric configurations', async () => { const source = new ESTermSource({ indexPatternTitle: indexPatternTitle, term: termFieldName, @@ -58,17 +58,16 @@ describe('getMetricFields', () => { }); const metrics = source.getMetricFields(); expect(metrics.length).toBe(2); - expect(metrics[0]).toEqual({ - type: 'sum', - field: sumFieldName, - propertyKey: '__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField', - propertyLabel: 'my custom label', - }); - expect(metrics[1]).toEqual({ - type: 'count', - propertyKey: '__kbnjoin__count_groupby_myIndex.myTermField', - propertyLabel: 'count of myIndex:myTermField', - }); + + expect(metrics[0].getAggType()).toEqual('sum'); + expect(metrics[0].getESDocFieldName()).toEqual(sumFieldName); + expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField'); + expect(await metrics[0].getLabel()).toEqual('my custom label'); + + expect(metrics[1].getAggType()).toEqual('count'); + expect(metrics[1].getName()).toEqual('__kbnjoin__count_groupby_myIndex.myTermField'); + expect(await metrics[1].getLabel()).toEqual('count of myIndex:myTermField'); + }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index c75c5600aaf92..ffccb18a69192 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -10,7 +10,8 @@ import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants'; +import { KibanaRegionField } from '../../fields/kibana_region_field'; export class KibanaRegionmapSource extends AbstractVectorSource { @@ -45,11 +46,20 @@ export class KibanaRegionmapSource extends AbstractVectorSource { ); }; + createField({ fieldName }) { + return new KibanaRegionField({ + fieldName, + source: this, + origin: FIELD_ORIGIN.SOURCE + }); + } + async getImmutableProperties() { return [ { label: getDataSourceLabel(), - value: KibanaRegionmapSource.title }, + value: KibanaRegionmapSource.title + }, { label: i18n.translate('xpack.maps.source.kbnRegionMap.vectorLayerLabel', { defaultMessage: 'Vector layer' @@ -59,7 +69,7 @@ export class KibanaRegionmapSource extends AbstractVectorSource { ]; } - async _getVectorFileMeta() { + async getVectorFileMeta() { const regionList = getKibanaRegionList(); const meta = regionList.find(source => source.name === this._descriptor.name); if (!meta) { @@ -75,7 +85,7 @@ export class KibanaRegionmapSource extends AbstractVectorSource { } async getGeoJsonWithMeta() { - const vectorFileMeta = await this._getVectorFileMeta(); + const vectorFileMeta = await this.getVectorFileMeta(); const featureCollection = await AbstractVectorSource.getGeoJson({ format: vectorFileMeta.format.type, featureCollectionPath: vectorFileMeta.meta.feature_collection_path, @@ -90,10 +100,8 @@ export class KibanaRegionmapSource extends AbstractVectorSource { } async getLeftJoinFields() { - const vectorFileMeta = await this._getVectorFileMeta(); - return vectorFileMeta.fields.map(f => { - return { name: f.name, label: f.description }; - }); + const vectorFileMeta = await this.getVectorFileMeta(); + return vectorFileMeta.fields.map(f => this.createField({ fieldName: f.name })); } async getDisplayName() { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js index 8f67d7618049b..e255e2478a37d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js @@ -48,6 +48,10 @@ export class AbstractVectorSource extends AbstractSource { })); } + createField() { + throw new Error(`Should implemement ${this.constructor.type} ${this}`); + } + _createDefaultLayerDescriptor(options, mapColors) { return VectorLayer.createDescriptor( { @@ -57,6 +61,10 @@ export class AbstractVectorSource extends AbstractSource { mapColors); } + _getTooltipPropertyNames() { + return this._tooltipFields.map(field => field.getName()); + } + createDefaultLayer(options, mapColors) { const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors); const style = new VectorStyle(layerDescriptor.style, this); @@ -131,4 +139,5 @@ export class AbstractVectorSource extends AbstractSource { getSourceTooltipContent(/* sourceDataRequest */) { return { tooltipContent: null, areResultsTrimmed: false }; } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js index 06709ba0ebf21..bb10b7686ae3b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js @@ -15,26 +15,54 @@ import { HEATMAP_COLOR_RAMP_LABEL } from '../heatmap_constants'; -export function HeatmapLegend({ colorRampName, label }) { - const header = colorRampName === DEFAULT_HEATMAP_COLOR_RAMP_NAME - ? - : ; - - return ( - - ); +export class HeatmapLegend extends React.Component { + + constructor() { + super(); + this.state = { label: '' }; + } + + componentDidUpdate() { + this._loadLabel(); + } + + componentDidMount() { + this._isMounted = true; + this._loadLabel(); + } + componentWillUnmount() { + this._isMounted = false; + } + + async _loadLabel() { + const label = await this.props.field.getLabel(); + if (this._isMounted && this.state.label !== label) { + this.setState({ label }); + } + } + + render() { + const colorRampName = this.props.colorRampName; + const header = colorRampName === DEFAULT_HEATMAP_COLOR_RAMP_NAME + ? + : ; + + return ( + + ); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index e537da8a3e2e4..e4982c86b53bb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -50,11 +50,11 @@ export class HeatmapStyle extends AbstractStyle { ); } - getLegendDetails(label) { + renderLegendDetails(field) { return ( ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js index 4d091389a360e..35c7066b7fd0f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js @@ -5,71 +5,13 @@ */ import _ from 'lodash'; -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { styleOptionShapes, rangeShape } from '../style_option_shapes'; -import { VectorStyle } from '../../vector_style'; -import { ColorGradient } from '../../../components/color_gradient'; -import { CircleIcon } from './circle_icon'; +import { rangeShape } from '../style_option_shapes'; import { getVectorStyleLabel } from '../get_vector_style_label'; -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { StyleLegendRow } from '../../../components/style_legend_row'; -function getLineWidthIcons() { - const defaultStyle = { - stroke: 'grey', - fill: 'none', - width: '12px', - }; - return [ - , - , - , - ]; -} - -function getSymbolSizeIcons() { - const defaultStyle = { - stroke: 'grey', - strokeWidth: 'none', - fill: 'grey', - }; - return [ - , - , - , - ]; -} - -function renderHeaderWithIcons(icons) { - return ( - - { - icons.map((icon, index) => { - const isLast = index === icons.length - 1; - let spacer; - if (!isLast) { - spacer = ( - - - - ); - } - return ( - - - {icon} - - {spacer} - - ); - }) - } - - ); -} - const EMPTY_VALUE = ''; export class StylePropertyLegendRow extends Component { @@ -97,19 +39,25 @@ export class StylePropertyLegendRow extends Component { } async _loadFieldFormatter() { - this._fieldValueFormatter = await this.props.getFieldFormatter(this.props.options.field); + if (this.props.style.isDynamic() && this.props.style.isComplete() && this.props.style.getField().getSource()) { + const field = this.props.style.getField(); + const source = field.getSource(); + this._fieldValueFormatter = await source.getFieldFormatter(field.getName()); + } else { + this._fieldValueFormatter = null; + } if (this._isMounted) { this.setState({ hasLoadedFieldFormatter: true }); } } _loadLabel = async () => { - if (this._isStatic()) { + if (this._excludeFromHeader()) { return; } // have to load label and then check for changes since field name stays constant while label may change - const label = await this.props.getFieldLabel(this.props.options.field.name); + const label = await this.props.style.getField().getLabel(); if (this._prevLabel === label) { return; } @@ -120,9 +68,8 @@ export class StylePropertyLegendRow extends Component { } } - _isStatic() { - return this.props.type === VectorStyle.STYLE_TYPE.STATIC || - !this.props.options.field || !this.props.options.field.name; + _excludeFromHeader() { + return !this.props.style.isDynamic() || !this.props.style.isComplete() || !this.props.style.getField().getName(); } _formatValue = value => { @@ -134,26 +81,19 @@ export class StylePropertyLegendRow extends Component { } render() { - const { name, options, range } = this.props; - if (this._isStatic()) { - return null; - } - let header; - if (options.color) { - header = ; - } else if (name === 'lineWidth') { - header = renderHeaderWithIcons(getLineWidthIcons()); - } else if (name === 'iconSize') { - header = renderHeaderWithIcons(getSymbolSizeIcons()); + const { range, style } = this.props; + if (this._excludeFromHeader()) { + return null; } + const header = style.renderHeader(); return ( ); @@ -161,10 +101,6 @@ export class StylePropertyLegendRow extends Component { } StylePropertyLegendRow.propTypes = { - name: PropTypes.string.isRequired, - type: PropTypes.string, - options: PropTypes.oneOfType(styleOptionShapes).isRequired, range: rangeShape, - getFieldLabel: PropTypes.func.isRequired, - getFieldFormatter: PropTypes.func.isRequired, + style: PropTypes.object }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js index 60baaff158377..e339cad6af973 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js @@ -7,34 +7,26 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { styleOptionShapes, rangeShape } from '../style_option_shapes'; +import { rangeShape } from '../style_option_shapes'; import { StylePropertyLegendRow } from './style_property_legend_row'; -export function VectorStyleLegend({ getFieldLabel, getFieldFormatter, styleProperties }) { +export function VectorStyleLegend({ styleProperties }) { return styleProperties.map(styleProperty => { return ( ); }); } const stylePropertyShape = PropTypes.shape({ - name: PropTypes.string.isRequired, - type: PropTypes.string, - options: PropTypes.oneOfType(styleOptionShapes).isRequired, range: rangeShape, + style: PropTypes.object }); VectorStyleLegend.propTypes = { - styleProperties: PropTypes.arrayOf(stylePropertyShape).isRequired, - getFieldLabel: PropTypes.func.isRequired, - getFieldFormatter: PropTypes.func.isRequired, + styleProperties: PropTypes.arrayOf(stylePropertyShape).isRequired }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js index d595dccaea425..a2edc8cb4f686 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js @@ -36,15 +36,6 @@ export const dynamicSizeShape = PropTypes.shape({ field: fieldShape, }); -export const styleOptionShapes = [ - staticColorShape, - dynamicColorShape, - staticOrientationShape, - dynamicOrientationShape, - staticSizeShape, - dynamicSizeShape -]; - export const rangeShape = PropTypes.shape({ min: PropTypes.number.isRequired, max: PropTypes.number.isRequired, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index f624f4e661a14..c8e4150fd2c26 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -52,20 +52,27 @@ export class VectorStyleEditor extends Component { async _loadOrdinalFields() { + const getFieldMeta = async (field) => { + return { + label: await field.getLabel(), + name: field.getName(), + origin: field.getOrigin() + }; + }; const dateFields = await this.props.layer.getDateFields(); - if (!this._isMounted) { - return; - } - if (!_.isEqual(dateFields, this.state.dateFields)) { - this.setState({ dateFields }); + const dateFieldPromises = dateFields.map(getFieldMeta); + const dateFieldsArray = await Promise.all(dateFieldPromises); + + if (this._isMounted && !_.isEqual(dateFieldsArray, this.state.dateFields)) { + this.setState({ dateFields: dateFieldsArray }); } const numberFields = await this.props.layer.getNumberFields(); - if (!this._isMounted) { - return; - } - if (!_.isEqual(numberFields, this.state.numberFields)) { - this.setState({ numberFields }); + const numberFieldPromises = numberFields.map(getFieldMeta); + + const numberFieldsArray = await Promise.all(numberFieldPromises); + if (this._isMounted && !_.isEqual(numberFieldsArray, this.state.numberFields)) { + this.setState({ numberFields: numberFieldsArray }); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 5c2122dfc4566..4b4b853c274cb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -9,11 +9,12 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import _ from 'lodash'; import { getComputedFieldName } from '../style_util'; import { getColorRampStops } from '../../color_utils'; +import { ColorGradient } from '../../components/color_gradient'; +import React from 'react'; export class DynamicColorProperty extends DynamicStyleProperty { - syncCircleColorWithMb(mbLayerId, mbMap, alpha) { const color = this._getMbColor(); mbMap.setPaintProperty(mbLayerId, 'circle-color', color); @@ -48,6 +49,18 @@ export class DynamicColorProperty extends DynamicStyleProperty { mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha); } + isCustomColorRamp() { + return !!this._options.customColorRamp; + } + + supportsFeatureState() { + return true; + } + + isScaled() { + return !this.isCustomColorRamp(); + } + _getMbColor() { const isDynamicConfigComplete = _.has(this._options, 'field.name') && _.has(this._options, 'color'); if (!isDynamicConfigComplete) { @@ -98,6 +111,14 @@ export class DynamicColorProperty extends DynamicStyleProperty { return getColorRampStops(this._options.color); } + renderHeader() { + if (this._options.color) { + return (); + } else { + return null; + } + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js index 2881ee422048b..fb4ffd8cce4b4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js @@ -22,6 +22,14 @@ export class DynamicOrientationProperty extends DynamicStyleProperty { } } + supportsFeatureState() { + return false; + } + + isScaled() { + return false; + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index 2758a440f57c5..bd011b27d81c8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -10,6 +10,34 @@ import { getComputedFieldName } from '../style_util'; import { HALF_LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE, SMALL_MAKI_ICON_SIZE } from '../symbol_utils'; import { vectorStyles } from '../vector_style_defaults'; import _ from 'lodash'; +import { CircleIcon } from '../components/legend/circle_icon'; +import React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; + +function getLineWidthIcons() { + const defaultStyle = { + stroke: 'grey', + fill: 'none', + width: '12px', + }; + return [ + , + , + , + ]; +} + +function getSymbolSizeIcons() { + const defaultStyle = { + stroke: 'grey', + fill: 'grey', + }; + return [ + , + , + , + ]; +} export class DynamicSizeProperty extends DynamicStyleProperty { @@ -79,6 +107,43 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _isSizeDynamicConfigComplete() { - return this._options.field && this._options.field.name && _.has(this._options, 'minSize') && _.has(this._options, 'maxSize'); + return this._field && this._field.isValid() && _.has(this._options, 'minSize') && _.has(this._options, 'maxSize'); + } + + renderHeader() { + let icons; + if (this.getStyleName() === vectorStyles.LINE_WIDTH) { + icons = getLineWidthIcons(); + } else if (this.getStyleName() === vectorStyles.ICON_SIZE) { + icons = getSymbolSizeIcons(); + } else { + return null; + } + + return ( + + { + icons.map((icon, index) => { + const isLast = index === icons.length - 1; + let spacer; + if (!isLast) { + spacer = ( + + + + ); + } + return ( + + + {icon} + + {spacer} + + ); + }) + } + + ); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 8200ede3e3523..e87bcc12c99be 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -6,7 +6,37 @@ import { AbstractStyleProperty } from './style_property'; +import { STYLE_TYPE } from '../../../../../common/constants'; export class DynamicStyleProperty extends AbstractStyleProperty { - static type = 'DYNAMIC'; + static type = STYLE_TYPE.DYNAMIC; + + constructor(options, styleName, field) { + super(options, styleName); + this._field = field; + } + + getField() { + return this._field; + } + + isDynamic() { + return true; + } + + isComplete() { + return !!this._field; + } + + getFieldOrigin() { + return this._field.getOrigin(); + } + + supportsFeatureState() { + return true; + } + + isScaled() { + return true; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_style_property.js index 448efc06899e5..6c53e00f8bd20 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_style_property.js @@ -6,8 +6,8 @@ import { AbstractStyleProperty } from './style_property'; +import { STYLE_TYPE } from '../../../../../common/constants'; export class StaticStyleProperty extends AbstractStyleProperty { - static type = 'STATIC'; - + static type = STYLE_TYPE.STATIC; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js index 7e9e27f83722d..9d182eac9fa8a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/style_property.js @@ -10,4 +10,30 @@ export class AbstractStyleProperty { this._options = options; this._styleName = styleName; } + + isDynamic() { + return false; + } + + /** + * Is the style fully defined and usable? (e.g. for rendering, in legend UX, ...) + * Why? during editing, partially-completed descriptors may be added to the layer-descriptor + * e.g. dynamic-fields can have an incomplete state when the field is not yet selected from the drop-down + * @returns {boolean} + */ + isComplete() { + return true; + } + + getStyleName() { + return this._styleName; + } + + getOptions() { + return this._options || {}; + } + + renderHeader() { + return null; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 70ebba7e8d177..2e2f10cc74935 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -6,17 +6,16 @@ import _ from 'lodash'; import React from 'react'; -import { i18n } from '@kbn/i18n'; import { VectorStyleEditor } from './components/vector_style_editor'; import { getDefaultProperties, vectorStyles } from './vector_style_defaults'; import { AbstractStyle } from '../abstract_style'; -import { SOURCE_DATA_ID_ORIGIN, GEO_JSON_TYPE } from '../../../../common/constants'; +import { GEO_JSON_TYPE, FIELD_ORIGIN, STYLE_TYPE } from '../../../../common/constants'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from './vector_constants'; import { getMakiSymbolAnchor } from './symbol_utils'; -import { getComputedFieldName, getComputedFieldNamePrefix } from './style_util'; +import { getComputedFieldName } from './style_util'; import { StaticStyleProperty } from './properties/static_style_property'; import { DynamicStyleProperty } from './properties/dynamic_style_property'; import { DynamicSizeProperty } from './properties/dynamic_size_property'; @@ -33,14 +32,22 @@ const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]; export class VectorStyle extends AbstractStyle { static type = 'VECTOR'; - static STYLE_TYPE = { 'DYNAMIC': DynamicStyleProperty.type, 'STATIC': StaticStyleProperty.type }; + static STYLE_TYPE = STYLE_TYPE; + static createDescriptor(properties = {}) { + return { + type: VectorStyle.type, + properties: { ...getDefaultProperties(), ...properties } + }; + } - static getComputedFieldName = getComputedFieldName; - static getComputedFieldNamePrefix = getComputedFieldNamePrefix; + static createDefaultStyleProperties(mapColors) { + return getDefaultProperties(mapColors); + } - constructor(descriptor = {}, source) { + constructor(descriptor = {}, source, layer) { super(); this._source = source; + this._layer = layer; this._descriptor = { ...descriptor, ...VectorStyle.createDescriptor(descriptor.properties), @@ -52,29 +59,21 @@ export class VectorStyle extends AbstractStyle { this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.ICON_SIZE], vectorStyles.ICON_SIZE); // eslint-disable-next-line max-len this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[vectorStyles.ICON_ORIENTATION], vectorStyles.ICON_ORIENTATION); - } - static createDescriptor(properties = {}) { - return { - type: VectorStyle.type, - properties: { ...getDefaultProperties(), ...properties } - }; } - static createDefaultStyleProperties(mapColors) { - return getDefaultProperties(mapColors); + _getAllStyleProperties() { + return [ + this._lineColorStyleProperty, + this._fillColorStyleProperty, + this._lineWidthStyleProperty, + this._iconSizeStyleProperty, + this._iconOrientationProperty + ]; } - static getDisplayName() { - return i18n.translate('xpack.maps.style.vector.displayNameLabel', { - defaultMessage: 'Vector style' - }); - } - - static description = ''; - renderEditor({ layer, onStyleDescriptorChange }) { - const styleProperties = { ...this.getProperties() }; + const styleProperties = { ...this.getRawProperties() }; const handlePropertyChange = (propertyName, settings) => { styleProperties[propertyName] = settings;//override single property, but preserve the rest const vectorStyleDescriptor = VectorStyle.createDescriptor(styleProperties); @@ -104,39 +103,45 @@ export class VectorStyle extends AbstractStyle { * can then use to update store state via dispatch. */ getDescriptorWithMissingStylePropsRemoved(nextOrdinalFields) { - const originalProperties = this.getProperties(); + + const originalProperties = this.getRawProperties(); const updatedProperties = {}; - Object.keys(originalProperties).forEach(propertyName => { - if (!this._isPropertyDynamic(propertyName)) { - return; - } - const fieldName = _.get(originalProperties[propertyName], 'options.field.name'); + const dynamicProperties = Object.keys(originalProperties).filter(key => { + const { type, options } = originalProperties[key] || {}; + return type === STYLE_TYPE.DYNAMIC && options.field && options.field.name; + }); + + dynamicProperties.forEach(key => { + + const dynamicProperty = originalProperties[key]; + const fieldName = dynamicProperty && dynamicProperty.options.field && dynamicProperty.options.field.name; if (!fieldName) { return; } - const matchingOrdinalField = nextOrdinalFields.find(oridinalField => { - return fieldName === oridinalField.name; + const matchingOrdinalField = nextOrdinalFields.find(ordinalField => { + return fieldName === ordinalField.getName(); }); if (matchingOrdinalField) { return; } - updatedProperties[propertyName] = { - type: VectorStyle.STYLE_TYPE.DYNAMIC, + updatedProperties[key] = { + type: DynamicStyleProperty.type, options: { - ...originalProperties[propertyName].options + ...originalProperties[key].options } }; - delete updatedProperties[propertyName].options.field; + delete updatedProperties[key].options.field; + }); if (Object.keys(updatedProperties).length === 0) { return { hasChanges: false, - nextStyleDescriptor: { ...this._descriptor }, + nextStyleDescriptor: { ...this._descriptor } }; } @@ -156,9 +161,9 @@ export class VectorStyle extends AbstractStyle { } const scaledFields = this.getDynamicPropertiesArray() - .map(({ options }) => { + .map(styleProperty => { return { - name: options.field.name, + name: styleProperty.getField().getName(), min: Infinity, max: -Infinity }; @@ -219,45 +224,22 @@ export class VectorStyle extends AbstractStyle { } getSourceFieldNames() { - const properties = this.getProperties(); const fieldNames = []; - Object.keys(properties).forEach(propertyName => { - if (!this._isPropertyDynamic(propertyName)) { - return; - } - - const field = _.get(properties[propertyName], 'options.field', {}); - if (field.origin === SOURCE_DATA_ID_ORIGIN && field.name) { - fieldNames.push(field.name); + this.getDynamicPropertiesArray().forEach(styleProperty => { + if (styleProperty.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { + fieldNames.push(styleProperty.getField().getName()); } }); - return fieldNames; } - getProperties() { + getRawProperties() { return this._descriptor.properties || {}; } getDynamicPropertiesArray() { - const styles = this.getProperties(); - return Object.keys(styles) - .map(styleName => { - const { type, options } = styles[styleName]; - return { - styleName, - type, - options - }; - }) - .filter(({ styleName }) => { - return this._isPropertyDynamic(styleName); - }); - } - - _isPropertyDynamic(propertyName) { - const { type, options } = _.get(this._descriptor, ['properties', propertyName], {}); - return type === VectorStyle.STYLE_TYPE.DYNAMIC && options.field && options.field.name; + const styleProperties = this._getAllStyleProperties(); + return styleProperties.filter(styleProperty => (styleProperty.isDynamic() && styleProperty.isComplete())); } _checkIfOnlyFeatureType = async (featureType) => { @@ -288,16 +270,12 @@ export class VectorStyle extends AbstractStyle { return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.LINE); } - _getIsPolygonsOnly = async () => { - return this._checkIfOnlyFeatureType(VECTOR_SHAPE_TYPES.POLYGON); - } - _getFieldRange = (fieldName) => { return _.get(this._descriptor, ['__styleMeta', fieldName]); } getIcon = () => { - const styles = this.getProperties(); + const styles = this.getRawProperties(); const symbolId = this.arePointsSymbolizedAsCircles() ? undefined : this._descriptor.properties.symbol.options.symbolId; @@ -305,65 +283,54 @@ export class VectorStyle extends AbstractStyle { ); } - getLegendDetails(getFieldLabel, getFieldFormatter) { - const styles = this.getProperties(); - const styleProperties = Object.keys(styles).map(styleName => { - const { type, options } = styles[styleName]; + renderLegendDetails() { + const styles = this._getAllStyleProperties(); + const styleProperties = styles.map((style) => { return { - name: styleName, - type, - options, - range: options && options.field && options.field.name ? this._getFieldRange(options.field.name) : null, + // eslint-disable-next-line max-len + range: (style.isDynamic() && style.isComplete() && style.getField().getName()) ? this._getFieldRange(style.getField().getName()) : null, + style: style }; }); return ( ); } _getStyleFields() { return this.getDynamicPropertiesArray() - .map(({ styleName, options }) => { - const name = options.field.name; + .map(styleProperty => { // "feature-state" data expressions are not supported with layout properties. // To work around this limitation, some styling values must fall back to geojson property values. let supportsFeatureState; let isScaled; - if (styleName === 'iconSize' + if (styleProperty.getStyleName() === vectorStyles.ICON_SIZE && this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON) { supportsFeatureState = false; isScaled = true; - } else if (styleName === 'iconOrientation') { - supportsFeatureState = false; - isScaled = false; - } else if ((styleName === vectorStyles.FILL_COLOR || styleName === vectorStyles.LINE_COLOR) - && options.useCustomColorRamp) { - supportsFeatureState = true; - isScaled = false; } else { - supportsFeatureState = true; - isScaled = true; + supportsFeatureState = styleProperty.supportsFeatureState(); + isScaled = styleProperty.isScaled(); } + const field = styleProperty.getField(); return { supportsFeatureState, isScaled, - name, - range: this._getFieldRange(name), - computedName: VectorStyle.getComputedFieldName(styleName, name), + name: field.getName(), + range: this._getFieldRange(field.getName()), + computedName: getComputedFieldName(styleProperty.getStyleName(), field.getName()), }; }); } @@ -472,13 +439,46 @@ export class VectorStyle extends AbstractStyle { } + arePointsSymbolizedAsCircles() { + return this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_CIRCLE; + } + + _makeField(fieldDescriptor) { + + if (!fieldDescriptor || !fieldDescriptor.name) { + return null; + } + + //fieldDescriptor.label is ignored. This is essentially cruft duplicating label-info from the metric-selection + //Ignore this custom label + if (fieldDescriptor.origin === FIELD_ORIGIN.SOURCE) { + return this._source.createField({ + fieldName: fieldDescriptor.name + }); + } else if (fieldDescriptor.origin === FIELD_ORIGIN.JOIN) { + let matchingField = null; + const joins = this._layer.getValidJoins(); + joins.find(join => { + const aggSource = join.getRightJoinSource(); + matchingField = aggSource.getMetricFieldForName(fieldDescriptor.name); + return !!matchingField; + }); + return matchingField; + } else { + throw new Error(`Unknown origin-type ${fieldDescriptor.origin}`); + } + + + } + _makeSizeProperty(descriptor, styleName) { if (!descriptor || !descriptor.options) { return new StaticSizeProperty({ size: 0 }, styleName); } else if (descriptor.type === StaticStyleProperty.type) { return new StaticSizeProperty(descriptor.options, styleName); } else if (descriptor.type === DynamicStyleProperty.type) { - return new DynamicSizeProperty(descriptor.options, styleName); + const field = this._makeField(descriptor.options.field); + return new DynamicSizeProperty(descriptor.options, styleName, field); } else { throw new Error(`${descriptor} not implemented`); } @@ -490,7 +490,8 @@ export class VectorStyle extends AbstractStyle { } else if (descriptor.type === StaticStyleProperty.type) { return new StaticColorProperty(descriptor.options, styleName); } else if (descriptor.type === DynamicStyleProperty.type) { - return new DynamicColorProperty(descriptor.options, styleName); + const field = this._makeField(descriptor.options.field); + return new DynamicColorProperty(descriptor.options, styleName, field); } else { throw new Error(`${descriptor} not implemented`); } @@ -502,7 +503,8 @@ export class VectorStyle extends AbstractStyle { } else if (descriptor.type === StaticStyleProperty.type) { return new StaticOrientationProperty(descriptor.options, styleName); } else if (descriptor.type === DynamicStyleProperty.type) { - return new DynamicOrientationProperty(descriptor.options, styleName); + const field = this._makeField(descriptor.options.field); + return new DynamicOrientationProperty(descriptor.options, styleName, field); } else { throw new Error(`${descriptor} not implemented`); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js index c0d76fadc01a5..a3020524a4e2a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js @@ -7,6 +7,35 @@ import { VectorStyle } from './vector_style'; import { DataRequest } from '../../util/data_request'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; +import { FIELD_ORIGIN } from '../../../../common/constants'; + +class MockField { + constructor({ fieldName }) { + this._fieldName = fieldName; + } + + getName() { + return this._fieldName; + } + + isValid() { + return !!this._fieldName; + } +} + +class MockSource { + + constructor({ supportedShapeTypes } = {}) { + this._supportedShapeTypes = supportedShapeTypes || Object.values(VECTOR_SHAPE_TYPES); + } + getSupportedShapeTypes() { + return this._supportedShapeTypes; + } + createField({ fieldName }) { + return new MockField({ fieldName }); + } +} + describe('getDescriptorWithMissingStylePropsRemoved', () => { const fieldName = 'doIStillExist'; @@ -17,29 +46,32 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { }, lineColor: { type: VectorStyle.STYLE_TYPE.DYNAMIC, - options: {} + options: { + 'field': { + 'name': fieldName, + 'origin': FIELD_ORIGIN.SOURCE + } + } }, iconSize: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: 'a color', - field: { name: fieldName } + field: { name: fieldName, origin: FIELD_ORIGIN.SOURCE } } } }; it('Should return no changes when next oridinal fields contain existing style property fields', () => { - const vectorStyle = new VectorStyle({ properties }); + const vectorStyle = new VectorStyle({ properties }, new MockSource()); - const nextOridinalFields = [ - { name: fieldName } - ]; + const nextOridinalFields = [new MockField({ fieldName })]; const { hasChanges } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextOridinalFields); expect(hasChanges).toBe(false); }); it('Should clear missing fields when next oridinal fields do not contain existing style property fields', () => { - const vectorStyle = new VectorStyle({ properties }); + const vectorStyle = new VectorStyle({ properties }, new MockSource()); const nextOridinalFields = []; const { hasChanges, nextStyleDescriptor } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextOridinalFields); @@ -83,12 +115,6 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { describe('pluckStyleMetaFromSourceDataRequest', () => { - const sourceMock = { - getSupportedShapeTypes: () => { - return Object.values(VECTOR_SHAPE_TYPES); - } - }; - describe('has features', () => { it('Should identify when feature collection only contains points', async () => { const sourceDataRequest = new DataRequest({ @@ -110,7 +136,7 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { ], } }); - const vectorStyle = new VectorStyle({}, sourceMock); + const vectorStyle = new VectorStyle({}, new MockSource()); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.hasFeatureType).toEqual({ @@ -140,7 +166,7 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { ], } }); - const vectorStyle = new VectorStyle({}, sourceMock); + const vectorStyle = new VectorStyle({}, new MockSource()); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.hasFeatureType).toEqual({ @@ -183,12 +209,13 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { field: { + origin: FIELD_ORIGIN.SOURCE, name: 'myDynamicFieldWithNoValues' } } } } - }, sourceMock); + }, new MockSource()); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.hasFeatureType).toEqual({ @@ -205,12 +232,13 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { field: { + origin: FIELD_ORIGIN.SOURCE, name: 'myDynamicField' } } } } - }, sourceMock); + }, new MockSource()); const featuresMeta = await vectorStyle.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); expect(featuresMeta.myDynamicField).toEqual({ @@ -226,32 +254,24 @@ describe('pluckStyleMetaFromSourceDataRequest', () => { describe('checkIfOnlyFeatureType', () => { describe('source supports single feature type', () => { - const sourceMock = { - getSupportedShapeTypes: () => { - return [VECTOR_SHAPE_TYPES.POINT]; - } - }; - it('isPointsOnly should be true when source feature type only supports points', async () => { - const vectorStyle = new VectorStyle({}, sourceMock); + const vectorStyle = new VectorStyle({}, new MockSource({ + supportedShapeTypes: [VECTOR_SHAPE_TYPES.POINT] + })); const isPointsOnly = await vectorStyle._getIsPointsOnly(); expect(isPointsOnly).toBe(true); }); it('isLineOnly should be false when source feature type only supports points', async () => { - const vectorStyle = new VectorStyle({}, sourceMock); + const vectorStyle = new VectorStyle({}, new MockSource({ + supportedShapeTypes: [VECTOR_SHAPE_TYPES.POINT] + })); const isLineOnly = await vectorStyle._getIsLinesOnly(); expect(isLineOnly).toBe(false); }); }); describe('source supports multiple feature types', () => { - const sourceMock = { - getSupportedShapeTypes: () => { - return Object.values(VECTOR_SHAPE_TYPES); - } - }; - it('isPointsOnly should be true when data contains just points', async () => { const vectorStyle = new VectorStyle({ __styleMeta: { @@ -261,7 +281,9 @@ describe('checkIfOnlyFeatureType', () => { POLYGON: false } } - }, sourceMock); + }, new MockSource({ + supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES) + })); const isPointsOnly = await vectorStyle._getIsPointsOnly(); expect(isPointsOnly).toBe(true); }); @@ -275,7 +297,9 @@ describe('checkIfOnlyFeatureType', () => { POLYGON: false } } - }, sourceMock); + }, new MockSource({ + supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES) + })); const isPointsOnly = await vectorStyle._getIsPointsOnly(); expect(isPointsOnly).toBe(false); }); @@ -289,7 +313,9 @@ describe('checkIfOnlyFeatureType', () => { POLYGON: true } } - }, sourceMock); + }, new MockSource({ + supportedShapeTypes: Object.values(VECTOR_SHAPE_TYPES) + })); const isPointsOnly = await vectorStyle._getIsPointsOnly(); expect(isPointsOnly).toBe(false); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js index 3dfe83ac83d02..70cfb6939e0d2 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/tile_layer.js @@ -12,10 +12,6 @@ export class TileLayer extends AbstractLayer { static type = LAYER_TYPE.TILE; - constructor({ layerDescriptor, source, style }) { - super({ layerDescriptor, source, style }); - } - static createDescriptor(options) { const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = TileLayer.type; diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js index 42629e192c27d..dce9ed479a4d7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js @@ -14,6 +14,7 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty { super(propertyKey, propertyName, rawValue, indexPattern); this._metricField = metricField; } + isFilterable() { return false; } @@ -22,10 +23,10 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty { if (typeof this._rawValue === 'undefined') { return '-'; } - if (this._metricField.type === METRIC_TYPE.COUNT || this._metricField.type === METRIC_TYPE.UNIQUE_COUNT) { + if (this._metricField.getAggType() === METRIC_TYPE.COUNT || this._metricField.getAggType() === METRIC_TYPE.UNIQUE_COUNT) { return this._rawValue; } - const indexPatternField = this._indexPattern.fields.getByName(this._metricField.field); + const indexPatternField = this._indexPattern.fields.getByName(this._metricField.getESDocFieldName()); if (!indexPatternField) { return this._rawValue; } diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js index cc19521063f36..ed9b284c12826 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js @@ -38,12 +38,14 @@ export class JoinTooltipProperty extends TooltipProperty { for (let i = 0; i < this._leftInnerJoins.length; i++) { const rightSource = this._leftInnerJoins[i].getRightJoinSource(); - const esTooltipProperty = await rightSource.createESTooltipProperty( - rightSource.getTerm(), - this._tooltipProperty.getRawValue() - ); - if (esTooltipProperty) { - esFilters.push(...(await esTooltipProperty.getESFilters())); + const termField = rightSource.getTermField(); + try { + const esTooltipProperty = await termField.createTooltipProperty(this._tooltipProperty.getRawValue()); + if (esTooltipProperty) { + esFilters.push(...(await esTooltipProperty.getESFilters())); + } + } catch(e) { + console.error('Cannot create joined filter', e); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 925ff963e05d4..362c7bfd72540 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -15,8 +15,7 @@ import { SOURCE_DATA_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, - LAYER_TYPE, - FIELD_ORIGIN, + LAYER_TYPE } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; @@ -91,9 +90,11 @@ export class VectorLayer extends AbstractLayer { this._joins = []; if (options.layerDescriptor.joins) { options.layerDescriptor.joins.forEach((joinDescriptor) => { - this._joins.push(new InnerJoin(joinDescriptor, this._source.getInspectorAdapters())); + const join = new InnerJoin(joinDescriptor, this._source); + this._joins.push(join); }); } + this._style = new VectorStyle(this._descriptor.style, this._source, this); } destroy() { @@ -181,26 +182,8 @@ export class VectorLayer extends AbstractLayer { return this._style.getDynamicPropertiesArray().length > 0; } - getLegendDetails() { - const getFieldLabel = async fieldName => { - const ordinalFields = await this._getOrdinalFields(); - const field = ordinalFields.find(({ name }) => { - return name === fieldName; - }); - - return field ? field.label : fieldName; - }; - - const getFieldFormatter = async field => { - const source = this._getFieldSource(field); - if (!source) { - return null; - } - - return await source.getFieldFormatter(field.name); - }; - - return this._style.getLegendDetails(getFieldLabel, getFieldFormatter); + renderLegendDetails() { + return this._style.renderLegendDetails(); } _getBoundsBasedOnData() { @@ -241,46 +224,24 @@ export class VectorLayer extends AbstractLayer { return this._source.getDisplayName(); } - async getDateFields() { - const timeFields = await this._source.getDateFields(); - return timeFields.map(({ label, name }) => { - return { - label, - name, - origin: SOURCE_DATA_ID_ORIGIN - }; - }); + return await this._source.getDateFields(); } - async getNumberFields() { - const numberFields = await this._source.getNumberFields(); - const numberFieldOptions = numberFields.map(({ label, name }) => { - return { - label, - name, - origin: FIELD_ORIGIN.SOURCE - }; - }); + const numberFieldOptions = await this._source.getNumberFields(); const joinFields = []; this.getValidJoins().forEach(join => { - const fields = join.getJoinFields().map(joinField => { - return { - ...joinField, - origin: FIELD_ORIGIN.JOIN, - }; - }); + const fields = join.getJoinFields(); joinFields.push(...fields); }); - return [...numberFieldOptions, ...joinFields]; } - async _getOrdinalFields() { + async getOrdinalFields() { return [ - ... await this.getDateFields(), - ... await this.getNumberFields() + ...await this.getDateFields(), + ...await this.getNumberFields() ]; } @@ -391,7 +352,7 @@ export class VectorLayer extends AbstractLayer { const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceId(); - const requestToken = Symbol(`layer-join-refresh:${ this.getId()} - ${sourceDataId}`); + const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); const searchFilters = { ...dataFilters, @@ -418,7 +379,7 @@ export class VectorLayer extends AbstractLayer { } = await joinSource.getPropertiesMap( searchFilters, leftSourceName, - join.getLeftFieldName(), + join.getLeftField().getName(), registerCancelCallback.bind(null, requestToken)); stopLoading(sourceDataId, requestToken, propertiesMap); return { @@ -450,9 +411,7 @@ export class VectorLayer extends AbstractLayer { const fieldNames = [ ...this._source.getFieldNames(), ...this._style.getSourceFieldNames(), - ...this.getValidJoins().map(join => { - return join.getLeftFieldName(); - }) + ...this.getValidJoins().map(join => join.getLeftField().getName()) ]; return { @@ -484,9 +443,8 @@ export class VectorLayer extends AbstractLayer { let isFeatureVisible = true; for (let j = 0; j < joinStates.length; j++) { const joinState = joinStates[j]; - const InnerJoin = joinState.join; - const rightMetricFields = InnerJoin.getRightMetricFields(); - const canJoinOnCurrent = InnerJoin.joinPropertiesToFeature(feature, joinState.propertiesMap, rightMetricFields); + const innerJoin = joinState.join; + const canJoinOnCurrent = innerJoin.joinPropertiesToFeature(feature, joinState.propertiesMap); isFeatureVisible = isFeatureVisible && canJoinOnCurrent; } @@ -506,7 +464,7 @@ export class VectorLayer extends AbstractLayer { startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { - const requestToken = Symbol(`layer-source-refresh:${ this.getId()} - source`); + const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); const searchFilters = this._getSearchFilters(dataFilters); const canSkip = await this._canSkipSourceUpdate(this._source, SOURCE_DATA_ID_ORIGIN, searchFilters); if (canSkip) { @@ -543,7 +501,7 @@ export class VectorLayer extends AbstractLayer { _assignIdsToFeatures(featureCollection) { //wrt https://github.com/elastic/kibana/issues/39317 - // In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. + //In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. //This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. //This is a work-around to avoid hitting such a worst-case //This was tested as a suitable work-around for mapbox-gl 0.54 @@ -770,7 +728,7 @@ export class VectorLayer extends AbstractLayer { const tooltipProperty = tooltipsFromSource[i]; const matchingJoins = []; for (let j = 0; j < this._joins.length; j++) { - if (this._joins[j].getLeftFieldName() === tooltipProperty.getPropertyKey()) { + if (this._joins[j].getLeftField().getName() === tooltipProperty.getPropertyKey()) { matchingJoins.push(this._joins[j]); } } @@ -806,28 +764,4 @@ export class VectorLayer extends AbstractLayer { return feature.properties[FEATURE_ID_PROPERTY_NAME] === id; }); } - - _getFieldSource(field) { - if (!field) { - return null; - } - - if (field.origin === FIELD_ORIGIN.SOURCE) { - return this._source; - } - - const join = this.getValidJoins().find(join => { - const matchingField = join.getJoinFields().find(joinField => { - return joinField.name === field.name; - }); - return !!matchingField; - }); - - if (!join) { - return null; - } - - return join.getRightJoinSource(); - } - } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js index d714ac3b092f6..7b7cf76cd365c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_tile_layer.js @@ -25,10 +25,6 @@ export class VectorTileLayer extends TileLayer { static type = LAYER_TYPE.VECTOR_TILE; - constructor({ layerDescriptor, source, style }) { - super({ layerDescriptor, source, style }); - } - static createDescriptor(options) { const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = VectorTileLayer.type; diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index 0e8b4959c9049..0b13db994193b 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -11,24 +11,22 @@ import { VectorTileLayer } from '../layers/vector_tile_layer'; import { VectorLayer } from '../layers/vector_layer'; import { HeatmapLayer } from '../layers/heatmap_layer'; import { ALL_SOURCES } from '../layers/sources/all_sources'; -import { VectorStyle } from '../layers/styles/vector/vector_style'; -import { HeatmapStyle } from '../layers/styles/heatmap/heatmap_style'; import { timefilter } from 'ui/timefilter'; import { getInspectorAdapters } from '../reducers/non_serializable_instances'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; function createLayerInstance(layerDescriptor, inspectorAdapters) { const source = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); - const style = createStyleInstance(layerDescriptor.style, source); + switch (layerDescriptor.type) { case TileLayer.type: - return new TileLayer({ layerDescriptor, source, style }); + return new TileLayer({ layerDescriptor, source }); case VectorLayer.type: - return new VectorLayer({ layerDescriptor, source, style }); + return new VectorLayer({ layerDescriptor, source }); case VectorTileLayer.type: - return new VectorTileLayer({ layerDescriptor, source, style }); + return new VectorTileLayer({ layerDescriptor, source }); case HeatmapLayer.type: - return new HeatmapLayer({ layerDescriptor, source, style }); + return new HeatmapLayer({ layerDescriptor, source }); default: throw new Error(`Unrecognized layerType ${layerDescriptor.type}`); } @@ -44,25 +42,6 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) { return new Source(sourceDescriptor, inspectorAdapters); } - -function createStyleInstance(styleDescriptor, source) { - - if (!styleDescriptor || !styleDescriptor.type) { - return null; - } - - switch (styleDescriptor.type) { - case 'TILE'://backfill for old tilestyles. - return null; - case VectorStyle.type: - return new VectorStyle(styleDescriptor, source); - case HeatmapStyle.type: - return new HeatmapStyle(styleDescriptor); - default: - throw new Error(`Unrecognized styleType ${styleDescriptor.type}`); - } -} - export const getTooltipState = ({ map }) => { return map.tooltipState; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5c5da391f08b5..a8978465b7b75 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6615,7 +6615,6 @@ "xpack.maps.source.wmsTitle": "ウェブマップサービス", "xpack.maps.style.heatmap.displayNameLabel": "ヒートマップスタイル", "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "解像度パラメーターが認識されません: {resolution}", - "xpack.maps.style.vector.displayNameLabel": "ベクタースタイル", "xpack.maps.styles.staticDynamic.dynamicDescription": "プロパティ値で特徴をシンボル化します。", "xpack.maps.styles.staticDynamic.staticDescription": "静的スタイルプロパティで特徴をシンボル化します。", "xpack.maps.styles.vector.borderColorLabel": "境界線の色", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cb4f0790e8310..582fafe8e782d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6617,7 +6617,6 @@ "xpack.maps.source.wmsTitle": "Web 地图服务", "xpack.maps.style.heatmap.displayNameLabel": "热图样式", "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "无法识别分辨率参数:{resolution}", - "xpack.maps.style.vector.displayNameLabel": "矢量样式", "xpack.maps.styles.staticDynamic.dynamicDescription": "使用属性值代表功能。", "xpack.maps.styles.staticDynamic.staticDescription": "使用静态样式属性代表功能。", "xpack.maps.styles.vector.borderColorLabel": "边框颜色", From c3c1a2be8a9ccfd970da3680f1cc0e193f913642 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 21 Nov 2019 16:45:24 -0500 Subject: [PATCH 005/128] [Maps] [Telemetry] Count indices with geo-fields (#51221) --- .../server/maps_telemetry/maps_telemetry.js | 42 +++--- .../maps_telemetry/maps_telemetry.test.js | 12 +- .../sample_index_pattern_saved_objects.json | 45 ++++++ .../sample_map_saved_objects.json | 128 +++++++++++++++++ .../test_resources/sample_saved_objects.json | 136 ------------------ 5 files changed, 205 insertions(+), 158 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json create mode 100644 x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json delete mode 100644 x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_saved_objects.json diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js index 0d318c41a7fd1..1c875322f2343 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { EMS_FILE, MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; +import { EMS_FILE, ES_GEO_FIELD_TYPE, MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; function getSavedObjectsClient(server, callCluster) { const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; @@ -32,8 +32,16 @@ function getUniqueLayerCounts(layerCountsList, mapsCount) { }, {}); } -export function buildMapsTelemetry(savedObjects, settings) { - const layerLists = savedObjects +function getIndexPatternsWithGeoFieldCount(indexPatterns) { + const fieldLists = indexPatterns.map(indexPattern => JSON.parse(indexPattern.attributes.fields)); + const fieldListsWithGeoFields = fieldLists.filter(fields => { + return fields.some(field => (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE)); + }); + return fieldListsWithGeoFields.length; +} + +export function buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings }) { + const layerLists = mapSavedObjects .map(savedMapObject => JSON.parse(savedMapObject.attributes.layerListJSON)); const mapsCount = layerLists.length; @@ -57,8 +65,11 @@ export function buildMapsTelemetry(savedObjects, settings) { const dataSourcesCountSum = _.sum(dataSourcesCount); const layersCountSum = _.sum(layersCount); + + const indexPatternsWithGeoFieldCount = getIndexPatternsWithGeoFieldCount(indexPatternSavedObjects); return { settings, + indexPatternsWithGeoFieldCount, // Total count of maps mapsTotalCount: mapsCount, // Time of capture @@ -88,24 +99,23 @@ export function buildMapsTelemetry(savedObjects, settings) { }; } -async function getSavedObjects(savedObjectsClient) { - const gisMapsSavedObject = await savedObjectsClient.find({ - type: MAP_SAVED_OBJECT_TYPE - }); - return _.get(gisMapsSavedObject, 'saved_objects'); +async function getMapSavedObjects(savedObjectsClient) { + const mapsSavedObjects = await savedObjectsClient.find({ type: MAP_SAVED_OBJECT_TYPE }); + return _.get(mapsSavedObjects, 'saved_objects', []); +} + +async function getIndexPatternSavedObjects(savedObjectsClient) { + const indexPatternSavedObjects = await savedObjectsClient.find({ type: 'index-pattern' }); + return _.get(indexPatternSavedObjects, 'saved_objects', []); } export async function getMapsTelemetry(server, callCluster) { const savedObjectsClient = getSavedObjectsClient(server, callCluster); - const savedObjects = await getSavedObjects(savedObjectsClient); + const mapSavedObjects = await getMapSavedObjects(savedObjectsClient); + const indexPatternSavedObjects = await getIndexPatternSavedObjects(savedObjectsClient); const settings = { showMapVisualizationTypes: server.config().get('xpack.maps.showMapVisualizationTypes') }; - const mapsTelemetry = buildMapsTelemetry(savedObjects, settings); - - return await savedObjectsClient.create('maps-telemetry', - mapsTelemetry, { - id: 'maps-telemetry', - overwrite: true, - }); + const mapsTelemetry = buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings }); + return await savedObjectsClient.create('maps-telemetry', mapsTelemetry, { id: 'maps-telemetry', overwrite: true }); } diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.test.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.test.js index 4f2b983a54028..c5b976e54865e 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.test.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as savedObjectsPayload from - './test_resources/sample_saved_objects.json'; +import mapSavedObjects from './test_resources/sample_map_saved_objects.json'; +import indexPatternSavedObjects from './test_resources/sample_index_pattern_saved_objects'; import { buildMapsTelemetry } from './maps_telemetry'; describe('buildMapsTelemetry', () => { @@ -15,10 +15,10 @@ describe('buildMapsTelemetry', () => { test('returns zeroed telemetry data when there are no saved objects', async () => { - const gisMaps = []; - const result = buildMapsTelemetry(gisMaps, settings); + const result = buildMapsTelemetry({ mapSavedObjects: [], indexPatternSavedObjects: [], settings }); expect(result).toMatchObject({ + indexPatternsWithGeoFieldCount: 0, attributesPerMap: { dataSourcesCount: { avg: 0, @@ -42,10 +42,10 @@ describe('buildMapsTelemetry', () => { test('returns expected telemetry data from saved objects', async () => { - const gisMaps = savedObjectsPayload.saved_objects; - const result = buildMapsTelemetry(gisMaps, settings); + const result = buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings }); expect(result).toMatchObject({ + indexPatternsWithGeoFieldCount: 2, attributesPerMap: { dataSourcesCount: { avg: 2.6666666666666665, diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json new file mode 100644 index 0000000000000..bb30a60f6d69f --- /dev/null +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json @@ -0,0 +1,45 @@ +[ + { + "attributes": { + "fields": "[{\"name\":\"geometry\",\"type\":\"geo_shape\",\"esTypes\":[\"geo_shape\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false}]", + "timeFieldName": "ORIG_DATE", + "title": "indexpattern-with-geoshape" + }, + "id": "4a7f6010-0aed-11ea-9dd2-95afd7ad44d4", + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-11-19T16:54:46.405Z", + "version": "Wzg0LDFd" + }, + { + "attributes": { + "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "indexpattern-with-geopoint" + }, + "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-11-19T20:05:37.607Z", + "version": "WzExMSwxXQ==" + }, + { + "attributes": { + "fields": "[{\"name\":\"assessment_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"date_exterior_condition\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"recording_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sale_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "title": "indexpattern-without-geo" + }, + "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-11-19T20:05:37.607Z", + "version": "WzExMSwxXQ==" + } +] diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json new file mode 100644 index 0000000000000..5bfe8ae38cac9 --- /dev/null +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json @@ -0,0 +1,128 @@ +[ + { + "type": "gis-map", + "id": "37b08d60-25b0-11e9-9858-0f3a1e60d007", + "attributes": { + "title": "Italy Map", + "description": "", + "mapStateJSON": "{\"zoom\":4.82,\"center\":{\"lon\":11.41545,\"lat\":42.0865},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"language\":\"lucene\",\"query\":\"\"}}", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"italy_provinces\"},\"id\":\"0oye8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#0c1f70\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"053fe296-f5ae-4cb0-9e73-a5752cb9ba74\",\"indexPatternId\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"geoField\":\"DestLocation\",\"requestType\":\"point\",\"resolution\":\"COARSE\"},\"id\":\"1gx22\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Greens\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}}},\"type\":\"VECTOR\"}]", + "uiStateJSON": "{}", + "bounds": { + "type": "polygon", + "coordinates": [ + [ + [ + -5.29778, + 51.54155 + ], + [ + -5.29778, + 30.98066 + ], + [ + 28.12868, + 30.98066 + ], + [ + 28.12868, + 51.54155 + ], + [ + -5.29778, + 51.54155 + ] + ] + ] + } + }, + "references": [ + ], + "updated_at": "2019-01-31T23:30:39.030Z", + "version": 1 + }, + { + "type": "gis-map", + "id": "5c061dc0-25af-11e9-9858-0f3a1e60d007", + "attributes": { + "title": "France Map", + "description": "", + "mapStateJSON": "{\"zoom\":3.43,\"center\":{\"lon\":-16.30411,\"lat\":42.88411},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\"},\"id\":\"65xbw\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.25,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#19c1e6\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"id\":\"240125db-e612-4001-b853-50107e55d984\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"id\":\"mdae9\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#1ce619\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", + "uiStateJSON": "{}", + "bounds": { + "type": "polygon", + "coordinates": [ + [ + [ + -59.97005, + 63.9123 + ], + [ + -59.97005, + 11.25616 + ], + [ + 27.36184, + 11.25616 + ], + [ + 27.36184, + 63.9123 + ], + [ + -59.97005, + 63.9123 + ] + ] + ] + } + }, + "references": [ + ], + "updated_at": "2019-01-31T23:24:30.492Z", + "version": 1 + }, + { + "type": "gis-map", + "id": "b853d5f0-25ae-11e9-9858-0f3a1e60d007", + "attributes": { + "title": "Canada Map", + "description": "", + "mapStateJSON": "{\"zoom\":2.12,\"center\":{\"lon\":-88.67592,\"lat\":34.23257},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"canada_provinces\"},\"id\":\"kt086\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#60895e\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", + "uiStateJSON": "{}", + "bounds": { + "type": "polygon", + "coordinates": [ + [ + [ + 163.37506, + 77.35215 + ], + [ + 163.37506, + -46.80667 + ], + [ + 19.2731, + -46.80667 + ], + [ + 19.2731, + 77.35215 + ], + [ + 163.37506, + 77.35215 + ] + ] + ] + } + }, + "references": [ + ], + "updated_at": "2019-01-31T23:19:55.855Z", + "version": 1 + } +] diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_saved_objects.json b/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_saved_objects.json deleted file mode 100644 index f5693027f585d..0000000000000 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/test_resources/sample_saved_objects.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "page":1, - "per_page":20, - "total":3, - "saved_objects":[ - { - "type":"gis-map", - "id":"37b08d60-25b0-11e9-9858-0f3a1e60d007", - "attributes":{ - "title":"Italy Map", - "description":"", - "mapStateJSON":"{\"zoom\":4.82,\"center\":{\"lon\":11.41545,\"lat\":42.0865},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"language\":\"lucene\",\"query\":\"\"}}", - "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"italy_provinces\"},\"id\":\"0oye8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#0c1f70\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"053fe296-f5ae-4cb0-9e73-a5752cb9ba74\",\"indexPatternId\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"geoField\":\"DestLocation\",\"requestType\":\"point\",\"resolution\":\"COARSE\"},\"id\":\"1gx22\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Greens\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}}},\"type\":\"VECTOR\"}]", - "uiStateJSON":"{}", - "bounds":{ - "type":"polygon", - "coordinates":[ - [ - [ - -5.29778, - 51.54155 - ], - [ - -5.29778, - 30.98066 - ], - [ - 28.12868, - 30.98066 - ], - [ - 28.12868, - 51.54155 - ], - [ - -5.29778, - 51.54155 - ] - ] - ] - } - }, - "references":[ - - ], - "updated_at":"2019-01-31T23:30:39.030Z", - "version":1 - }, - { - "type":"gis-map", - "id":"5c061dc0-25af-11e9-9858-0f3a1e60d007", - "attributes":{ - "title":"France Map", - "description":"", - "mapStateJSON":"{\"zoom\":3.43,\"center\":{\"lon\":-16.30411,\"lat\":42.88411},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", - "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\"},\"id\":\"65xbw\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.25,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#19c1e6\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"id\":\"240125db-e612-4001-b853-50107e55d984\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"id\":\"mdae9\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#1ce619\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", - "uiStateJSON":"{}", - "bounds":{ - "type":"polygon", - "coordinates":[ - [ - [ - -59.97005, - 63.9123 - ], - [ - -59.97005, - 11.25616 - ], - [ - 27.36184, - 11.25616 - ], - [ - 27.36184, - 63.9123 - ], - [ - -59.97005, - 63.9123 - ] - ] - ] - } - }, - "references":[ - - ], - "updated_at":"2019-01-31T23:24:30.492Z", - "version":1 - }, - { - "type":"gis-map", - "id":"b853d5f0-25ae-11e9-9858-0f3a1e60d007", - "attributes":{ - "title":"Canada Map", - "description":"", - "mapStateJSON":"{\"zoom\":2.12,\"center\":{\"lon\":-88.67592,\"lat\":34.23257},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", - "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"canada_provinces\"},\"id\":\"kt086\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#60895e\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", - "uiStateJSON":"{}", - "bounds":{ - "type":"polygon", - "coordinates":[ - [ - [ - 163.37506, - 77.35215 - ], - [ - 163.37506, - -46.80667 - ], - [ - 19.2731, - -46.80667 - ], - [ - 19.2731, - 77.35215 - ], - [ - 163.37506, - 77.35215 - ] - ] - ] - } - }, - "references":[ - - ], - "updated_at":"2019-01-31T23:19:55.855Z", - "version":1 - } - ] -} From 1142f4b2771a07c095aa932f437d2e9fe27c4cc2 Mon Sep 17 00:00:00 2001 From: Janeen Mikell-Straughn <57149392+jmikell821@users.noreply.github.com> Date: Thu, 21 Nov 2019 16:46:34 -0500 Subject: [PATCH 006/128] Added endgame-* index and new heading 3 Elastic Endpoint SMP. (#51071) --- docs/siem/index.asciidoc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/siem/index.asciidoc b/docs/siem/index.asciidoc index c947e000c8138..f56baf6abdc2e 100644 --- a/docs/siem/index.asciidoc +++ b/docs/siem/index.asciidoc @@ -24,7 +24,7 @@ Kibana provides step-by-step instructions to help you add data. The detailed information and instructions. [float] -=== {Beats} +=== {Beats} https://www.elastic.co/products/beats/auditbeat[{auditbeat}], https://www.elastic.co/products/beats/filebeat[{filebeat}], @@ -33,9 +33,14 @@ https://www.elastic.co/products/beats/packetbeat[{packetbeat}] send security events and other data to Elasticsearch. The default index patterns for SIEM events are `auditbeat-*`, `winlogbeat-*`, -`filebeat-*`, and `packetbeat-*``. You can change the default index patterns in +`filebeat-*`, `endgame-*`, and `packetbeat-*``. You can change the default index patterns in *Kibana > Management > Advanced Settings > siem:defaultIndex*. +[float] +=== Elastic Endpoint Sensor Management Platform + +The Elastic Endpoint Sensor Management Platform (SMP) ships host and network events directly to the SIEM application, and is fully ECS compliant. + [float] === Elastic Common Schema (ECS) for normalizing data From 8acfd4673ce0757cc9fb3f64500d1b34983c6d0b Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 21 Nov 2019 15:23:40 -0700 Subject: [PATCH 007/128] reset locked emotion/* and csstype versions (#51366) --- yarn.lock | 74 ++++--------------------------------------------------- 1 file changed, 5 insertions(+), 69 deletions(-) diff --git a/yarn.lock b/yarn.lock index 33fc751813764..b2370dc411cc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -998,7 +998,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@7.5.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.3", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5": +"@babel/runtime@7.5.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== @@ -1320,16 +1320,6 @@ resolved "https://registry.yarnpkg.com/@elastic/ui-ace/-/ui-ace-0.2.3.tgz#5281aed47a79b7216c55542b0675e435692f20cd" integrity sha512-Nti5s2dplBPhSKRwJxG9JXTMOev4jVOWcnTJD1TOkJr1MUBYKVZcNcJtIVMSvahWGmP0B/UfO9q9lyRqdivkvQ== -"@emotion/cache@^10.0.15": - version "10.0.15" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.15.tgz#b81767b48015aae2689c60373992145c67b8de02" - integrity sha512-8VthgeKhlGeTXSW1JN7I14AnAaiFPbOrqNqg3dPoGCZ3bnMjkrmRU0zrx0BtBw9esBaPaQgDB9y0tVgAGT2Mrg== - dependencies: - "@emotion/sheet" "0.9.3" - "@emotion/stylis" "0.8.4" - "@emotion/utils" "0.11.2" - "@emotion/weak-memoize" "0.2.3" - "@emotion/cache@^10.0.17", "@emotion/cache@^10.0.9": version "10.0.19" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.19.tgz#d258d94d9c707dcadaf1558def968b86bb87ad71" @@ -1340,7 +1330,7 @@ "@emotion/utils" "0.11.2" "@emotion/weak-memoize" "0.2.4" -"@emotion/core@^10.0.14": +"@emotion/core@^10.0.14", "@emotion/core@^10.0.9": version "10.0.22" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.22.tgz#2ac7bcf9b99a1979ab5b0a876fbf37ab0688b177" integrity sha512-7eoP6KQVUyOjAkE6y4fdlxbZRA4ILs7dqkkm6oZUJmihtHv0UBq98VgPirq9T8F9K2gKu0J/au/TpKryKMinaA== @@ -1352,27 +1342,6 @@ "@emotion/sheet" "0.9.3" "@emotion/utils" "0.11.2" -"@emotion/core@^10.0.9": - version "10.0.16" - resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.16.tgz#e43630b65c84e31e81f34db3286eab584b08cfaa" - integrity sha512-whbiiA7FfPreBY4BqWky2qRfAZvq+4dKQ1WNJuiYQwPCNmb0pEYDgNheSbZoNKtGTtfPaM28hBbZAKWD5EZXmQ== - dependencies: - "@babel/runtime" "^7.4.3" - "@emotion/cache" "^10.0.15" - "@emotion/css" "^10.0.14" - "@emotion/serialize" "^0.11.9" - "@emotion/sheet" "0.9.3" - "@emotion/utils" "0.11.2" - -"@emotion/css@^10.0.14": - version "10.0.14" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.14.tgz#95dacabdd0e22845d1a1b0b5968d9afa34011139" - integrity sha512-MozgPkBEWvorcdpqHZE5x1D/PLEHUitALQCQYt2wayf4UNhpgQs2tN0UwHYS4FMy5ROBH+0ALyCFVYJ/ywmwlg== - dependencies: - "@emotion/serialize" "^0.11.8" - "@emotion/utils" "0.11.2" - babel-plugin-emotion "^10.0.14" - "@emotion/css@^10.0.22", "@emotion/css@^10.0.9": version "10.0.22" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.22.tgz#37b1abb6826759fe8ac0af0ac0034d27de6d1793" @@ -1392,20 +1361,13 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.3.tgz#a166882c81c0c6040975dd30df24fae8549bd96f" integrity sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw== -"@emotion/is-prop-valid@0.8.5": +"@emotion/is-prop-valid@0.8.5", "@emotion/is-prop-valid@^0.8.3": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.5.tgz#2dda0791f0eafa12b7a0a5b39858405cc7bde983" integrity sha512-6ZODuZSFofbxSbcxwsFz+6ioPjb0ISJRRPLZ+WIbjcU2IMU0Io+RGQjjaTgOvNQl007KICBm7zXQaYQEC1r6Bg== dependencies: "@emotion/memoize" "0.7.3" -"@emotion/is-prop-valid@^0.8.3": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.4.tgz#cf1dcfc1812c226f05e1ba53592eb6b51e734990" - integrity sha512-QBW8h6wVQgeQ55F52rNaprEJxtVR+/ScOP8/V1ScSpPzKqHdFB9QVqby0Z50sqS8mcaeIl5vR1vQpKwJbIS6NQ== - dependencies: - "@emotion/memoize" "0.7.3" - "@emotion/memoize@0.7.2": version "0.7.2" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.2.tgz#7f4c71b7654068dfcccad29553520f984cc66b30" @@ -1416,7 +1378,7 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78" integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow== -"@emotion/serialize@^0.11.12", "@emotion/serialize@^0.11.14": +"@emotion/serialize@^0.11.12", "@emotion/serialize@^0.11.14", "@emotion/serialize@^0.11.9": version "0.11.14" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.14.tgz#56a6d8d04d837cc5b0126788b2134c51353c6488" integrity sha512-6hTsySIuQTbDbv00AnUO6O6Xafdwo5GswRlMZ5hHqiFx+4pZ7uGWXUQFW46Kc2taGhP89uXMXn/lWQkdyTosPA== @@ -1427,17 +1389,6 @@ "@emotion/utils" "0.11.2" csstype "^2.5.7" -"@emotion/serialize@^0.11.8", "@emotion/serialize@^0.11.9": - version "0.11.9" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.9.tgz#123e0f51d2dee9693fae1057bd7fc27b021d6868" - integrity sha512-/Cn4V81z3ZyFiDQRw8nhGFaHkxHtmCSSBUit4vgTuLA1BqxfJUYiqSq97tq/vV8z9LfIoqs6a9v6QrUFWZpK7A== - dependencies: - "@emotion/hash" "0.7.2" - "@emotion/memoize" "0.7.2" - "@emotion/unitless" "0.7.4" - "@emotion/utils" "0.11.2" - csstype "^2.5.7" - "@emotion/sheet@0.9.3": version "0.9.3" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a" @@ -1476,11 +1427,6 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183" integrity sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA== -"@emotion/weak-memoize@0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.3.tgz#dfa0c92efe44a1d1a7974fb49ffeb40ef2da5a27" - integrity sha512-zVgvPwGK7c1aVdUVc9Qv7SqepOGRDrqCw7KZPSZziWGxSlbII3gmvGLPzLX4d0n0BMbamBacUrN22zOMyFFEkQ== - "@emotion/weak-memoize@0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" @@ -9182,17 +9128,7 @@ cssstyle@^1.1.1: dependencies: cssom "0.3.x" -csstype@^2.2.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01" - integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow== - -csstype@^2.5.7: - version "2.6.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.3.tgz#b701e5968245bf9b08d54ac83d00b624e622a9fa" - integrity sha512-rINUZXOkcBmoHWEyu7JdHu5JMzkGRoMX4ov9830WNgxf5UYxcBUO0QTKAqeJ5EZfSdlrcJYkC8WwfVW7JYi4yg== - -csstype@^2.6.7: +csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== From a9258ffa5eb951a5a5db928da01b9522de0b81db Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 21 Nov 2019 15:46:00 -0700 Subject: [PATCH 008/128] [Maps] avoid duplicated geometry in filter meta (#51133) * [Maps] avoid duplicated geometry in filter meta * clean up * fix type error * remove filterTypes from data shim and import directly from NP data plugin * rename mapQueryDsl to mapSpatialFilter * add unit test for mapSpatialFilter --- .../data/common/es_query/filters/types.ts | 1 + .../query/filter_manager/lib/map_filter.ts | 2 + .../lib/mappers/map_spatial_filter.test.ts | 84 +++++++++++++++++++ .../lib/mappers/map_spatial_filter.ts | 40 +++++++++ .../feature_geometry_filter_form.js | 5 +- .../maps/public/elasticsearch_geo_utils.js | 2 + .../public/elasticsearch_geo_utils.test.js | 5 ++ .../plugins/maps/public/kibana_services.js | 2 + 8 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts create mode 100644 src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts diff --git a/src/plugins/data/common/es_query/filters/types.ts b/src/plugins/data/common/es_query/filters/types.ts index a242df4811c05..01a921fc88ae7 100644 --- a/src/plugins/data/common/es_query/filters/types.ts +++ b/src/plugins/data/common/es_query/filters/types.ts @@ -48,4 +48,5 @@ export enum FILTERS { RANGE = 'range', GEO_BOUNDING_BOX = 'geo_bounding_box', GEO_POLYGON = 'geo_polygon', + SPATIAL_FILTER = 'spatial_filter', } diff --git a/src/plugins/data/public/query/filter_manager/lib/map_filter.ts b/src/plugins/data/public/query/filter_manager/lib/map_filter.ts index a68eafe6bf1c2..dc3deb93bd27b 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_filter.ts @@ -19,6 +19,7 @@ import { reduceRight } from 'lodash'; +import { mapSpatialFilter } from './mappers/map_spatial_filter'; import { mapMatchAll } from './mappers/map_match_all'; import { mapPhrase } from './mappers/map_phrase'; import { mapPhrases } from './mappers/map_phrases'; @@ -50,6 +51,7 @@ export function mapFilter(filter: esFilters.Filter) { // that either handles the mapping operation or not // and add it here. ProTip: These are executed in order listed const mappers = [ + mapSpatialFilter, mapMatchAll, mapRange, mapPhrase, diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts new file mode 100644 index 0000000000000..fdd029c563cdd --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mapSpatialFilter } from './map_spatial_filter'; +import { esFilters } from '../../../../../common'; + +describe('mapSpatialFilter()', () => { + test('should return the key for matching multi polygon filter', async () => { + const filter = { + meta: { + alias: 'my spatial filter', + type: esFilters.FILTERS.SPATIAL_FILTER, + } as esFilters.FilterMeta, + query: { + bool: { + should: [ + { + geo_polygon: { + geoCoordinates: { points: [] }, + }, + }, + ], + }, + }, + } as esFilters.Filter; + const result = mapSpatialFilter(filter); + + expect(result).toHaveProperty('key', 'query'); + expect(result).toHaveProperty('value', ''); + expect(result).toHaveProperty('type', esFilters.FILTERS.SPATIAL_FILTER); + }); + + test('should return the key for matching polygon filter', async () => { + const filter = { + meta: { + alias: 'my spatial filter', + type: esFilters.FILTERS.SPATIAL_FILTER, + } as esFilters.FilterMeta, + geo_polygon: { + geoCoordinates: { points: [] }, + }, + } as esFilters.Filter; + const result = mapSpatialFilter(filter); + + expect(result).toHaveProperty('key', 'geo_polygon'); + expect(result).toHaveProperty('value', ''); + expect(result).toHaveProperty('type', esFilters.FILTERS.SPATIAL_FILTER); + }); + + test('should return undefined for none matching', async done => { + const filter = { + meta: { + alias: 'my spatial filter', + } as esFilters.FilterMeta, + geo_polygon: { + geoCoordinates: { points: [] }, + }, + } as esFilters.Filter; + + try { + mapSpatialFilter(filter); + } catch (e) { + expect(e).toBe(filter); + + done(); + } + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts new file mode 100644 index 0000000000000..3cf1cf7835e69 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { esFilters } from '../../../../../common'; + +// Use mapSpatialFilter mapper to avoid bloated meta with value and params for spatial filters. +export const mapSpatialFilter = (filter: esFilters.Filter) => { + const metaProperty = /(^\$|meta)/; + const key = Object.keys(filter).find(item => { + return !item.match(metaProperty); + }); + if ( + key && + filter.meta && + filter.meta.alias && + filter.meta.type === esFilters.FILTERS.SPATIAL_FILTER + ) { + return { + key, + type: filter.meta.type, + value: '', + }; + } + throw filter; +}; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js index 5552a0f4bd5ee..be21e7d1f9858 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js @@ -14,6 +14,9 @@ import { GeometryFilterForm } from '../../../components/geometry_filter_form'; import { UrlOverflowService } from 'ui/error_url_overflow'; import rison from 'rison-node'; +// over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped. +const META_OVERHEAD = 100; + const urlOverflow = new UrlOverflowService(); export class FeatureGeometryFilterForm extends Component { @@ -70,7 +73,7 @@ export class FeatureGeometryFilterForm extends Component { // Ensure filter will not overflow URL. Filters that contain geometry can be extremely large. // No elasticsearch support for pre-indexed shapes and geo_point spatial queries. - if (window.location.href.length + rison.encode(filter).length > urlOverflow.failLength()) { + if (window.location.href.length + rison.encode(filter).length + META_OVERHEAD > urlOverflow.failLength()) { this.setState({ errorMsg: i18n.translate('xpack.maps.tooltip.geometryFilterForm.filterTooLargeMessage', { defaultMessage: 'Cannot create filter. Filters are added to the URL, and this shape has too many vertices to fit in the URL.' diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index 5643bcba26816..ef2819f1f372c 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -18,6 +18,7 @@ import { LAT_INDEX, } from '../common/constants'; import { getEsSpatialRelationLabel } from '../common/i18n_getters'; +import { SPATIAL_FILTER_TYPE } from './kibana_services'; function ensureGeoField(type) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; @@ -287,6 +288,7 @@ function createGeometryFilterWithMeta({ }) : getEsSpatialRelationLabel(relation); const meta = { + type: SPATIAL_FILTER_TYPE, negate: false, index: indexPatternId, alias: `${geoFieldName} ${relationLabel} ${geometryLabel}` diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js index 69f65a5cd11e3..0b84b4c32f4ac 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -6,6 +6,11 @@ jest.mock('ui/new_platform'); jest.mock('ui/index_patterns'); +jest.mock('./kibana_services', () => { + return { + SPATIAL_FILTER_TYPE: 'spatial_filter' + }; +}); import { hitsToGeoJson, diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index e2500d7331db6..12fab24d1f8d6 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -7,7 +7,9 @@ import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; export { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { esFilters } from '../../../../../src/plugins/data/public'; +export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; export { SearchSource } from 'ui/courier'; export const indexPatternService = data.indexPatterns.indexPatterns; From 5974c4658fe527d4ba4313bf5dc2f3bb3d858e9d Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Thu, 21 Nov 2019 18:33:15 -0500 Subject: [PATCH 009/128] Remove @elastic/lsp-extension (#51335) This was only used by the code plugin, which was removed in 7.5.0. --- x-pack/package.json | 1 - yarn.lock | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index b723d6648e364..927efbffa132e 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -181,7 +181,6 @@ "@elastic/ems-client": "1.0.5", "@elastic/eui": "16.0.0", "@elastic/filesaver": "1.1.2", - "@elastic/lsp-extension": "^0.1.2", "@elastic/maki": "6.1.0", "@elastic/node-crypto": "^1.0.0", "@elastic/numeral": "2.3.3", diff --git a/yarn.lock b/yarn.lock index b2370dc411cc8..7f33c00c5460a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1264,14 +1264,6 @@ oppsy "2.x.x" pumpify "1.3.x" -"@elastic/lsp-extension@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@elastic/lsp-extension/-/lsp-extension-0.1.2.tgz#7356d951d272e833d02a81e13a0ef710f9474195" - integrity sha512-yDj5Ht5KCHDwBlgrlusmLtV/Yxa5z2f3vMSYbNFotoRMup8345/ZwlFp/zmyl04iFOVpT8ouB34+Ttpzbpd3vA== - dependencies: - vscode-languageserver "^5.2.1" - vscode-languageserver-types "^3.14.0" - "@elastic/makelogs@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@elastic/makelogs/-/makelogs-5.0.0.tgz#0064e9009c4e480d17195ab70d627bc07635540f" @@ -29234,7 +29226,7 @@ vscode-languageserver-protocol@3.14.1: vscode-jsonrpc "^4.0.0" vscode-languageserver-types "3.14.0" -vscode-languageserver-types@3.14.0, vscode-languageserver-types@^3.14.0: +vscode-languageserver-types@3.14.0: version "3.14.0" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== From 2b04ba199c84570d85fd23b14e858a216370c931 Mon Sep 17 00:00:00 2001 From: Jose Sanchez Robles Date: Fri, 22 Nov 2019 01:16:46 +0100 Subject: [PATCH 010/128] Added Wazuh plugin to known plugins list (#50751) --- docs/plugins/known-plugins.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index 58885ae04605d..cc27eca4c267e 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -15,6 +15,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy * https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation * https://github.com/samtecspg/conveyor[Conveyor] - Simple (GUI) interface for importing data into Elasticsearch. +* https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. * https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. * https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API * https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. From b934f8dd49a23fefcc3adfb74f76b1a314410b46 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 21 Nov 2019 17:29:56 -0700 Subject: [PATCH 011/128] [master] Polish SHA comparison in reference doc (#46432) (#51407) * Polish SHA comparison in reference doc * Update targz.asciidoc --- docs/setup/install/targz.asciidoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/setup/install/targz.asciidoc b/docs/setup/install/targz.asciidoc index 138431aca22fa..e3104520292ff 100644 --- a/docs/setup/install/targz.asciidoc +++ b/docs/setup/install/targz.asciidoc @@ -32,13 +32,13 @@ The Linux archive for Kibana v{version} can be downloaded and installed as follo ["source","sh",subs="attributes"] -------------------------------------------- -wget https://artifacts.elastic.co/downloads/kibana/kibana-{version}-linux-x86_64.tar.gz -shasum -a 512 kibana-{version}-linux-x86_64.tar.gz <1> +curl -O https://artifacts.elastic.co/downloads/kibana/kibana-{version}-linux-x86_64.tar.gz +curl https://artifacts.elastic.co/downloads/kibana/kibana-{version}-linux-x86_64.tar.gz.sha512 | shasum -a 512 -c - <1> tar -xzf kibana-{version}-linux-x86_64.tar.gz cd kibana-{version}-linux-x86_64/ <2> -------------------------------------------- -<1> Compare the SHA produced by `shasum` with the - https://artifacts.elastic.co/downloads/kibana/kibana-{version}-linux-x86_64.tar.gz.sha512[published SHA]. +<1> Compares the SHA of the downloaded `.tar.gz` archive and the published checksum, which should output + `kibana-{version}-linux-x86_64.tar.gz: OK`. <2> This directory is known as `$KIBANA_HOME`. endif::[] @@ -60,12 +60,12 @@ The Darwin archive for Kibana v{version} can be downloaded and installed as foll ["source","sh",subs="attributes"] -------------------------------------------- curl -O https://artifacts.elastic.co/downloads/kibana/kibana-{version}-darwin-x86_64.tar.gz -shasum -a 512 kibana-{version}-darwin-x86_64.tar.gz <1> +curl https://artifacts.elastic.co/downloads/kibana/kibana-{version}-darwin-x86_64.tar.gz.sha512 | shasum -a 512 -c - <1> tar -xzf kibana-{version}-darwin-x86_64.tar.gz cd kibana-{version}-darwin-x86_64/ <2> -------------------------------------------- -<1> Compare the SHA produced by `shasum` with the - https://artifacts.elastic.co/downloads/kibana/kibana-{version}-darwin-x86_64.tar.gz.sha512[published SHA]. +<1> Compares the SHA of the downloaded `.tar.gz` archive and the published checksum, which should output + `kibana-{version}-darwin-x86_64.tar.gz: OK`. <2> This directory is known as `$KIBANA_HOME`. Alternatively, you can download the following package, which contains only From 90db0b8101d1d5f546f5a347ca8710ec54fa70eb Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 21 Nov 2019 18:28:24 -0700 Subject: [PATCH 012/128] disable flaky suite (#40912) --- test/functional/apps/visualize/_visualize_listing.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/_visualize_listing.js b/test/functional/apps/visualize/_visualize_listing.js index df4812ab3f147..71ca5e25e83e5 100644 --- a/test/functional/apps/visualize/_visualize_listing.js +++ b/test/functional/apps/visualize/_visualize_listing.js @@ -22,7 +22,8 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'header', 'common']); - describe('visualize listing page', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/40912 + describe.skip('visualize listing page', function describeIndexTests() { const vizName = 'Visualize Listing Test'; describe('create and delete', function () { From 219397092881c8e6e82ddb56c3830c5898e5064f Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 22 Nov 2019 13:05:54 +1100 Subject: [PATCH 013/128] Update explanation of elasticsearch.ssl.key (#50748) The docs for `elasticsearch.ssl.certificate` and `elasticsearch.ssl.key` were not entirely accurate and referenced an out of date ES setting. --- docs/setup/settings.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index f4434ea7a09f4..f2c06a3737c7c 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -93,8 +93,8 @@ the configured certificate. `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Optional settings that provide the paths to the PEM-format SSL certificate and key files. These files are used to verify the identity of Kibana to Elasticsearch and are -required when `xpack.ssl.verification_mode` in Elasticsearch is set to either -`certificate` or `full`. +required when `xpack.security.http.ssl.client_authentication` in Elasticsearch is +set to `required`. `elasticsearch.ssl.certificateAuthorities:`:: Optional setting that enables you to specify a list of paths to the PEM file for the certificate authority for From fd4df6bcf858c76252b9acb2061caf856d3f6516 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 21 Nov 2019 19:23:01 -0700 Subject: [PATCH 014/128] Allows empty string for query when filters are set or ommiting them all together (#51398) Fixes a bug to allow an empty query string when filters are set or to omit the query on the post call if you have a set of filters defined. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../detection_engine/routes/schemas.test.ts | 76 +++++++++++++++++++ .../lib/detection_engine/routes/schemas.ts | 9 ++- .../signals/filter_with_empty_query.json | 53 +++++++++++++ .../scripts/signals/filter_without_query.json | 52 +++++++++++++ 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 8273e9942da1a..5e5f37ca8a080 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -984,6 +984,82 @@ describe('schemas', () => { }).error ).toBeTruthy(); }); + + test('You can have an empty query string when filters are present', () => { + expect( + createSignalsSchema.validate< + Partial & { meta: string }> + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: '', + language: 'kuery', + filters: [], + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can omit the query string when filters are present', () => { + expect( + createSignalsSchema.validate< + Partial & { meta: string }> + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('query string defaults to empty string when present with filters', () => { + expect( + createSignalsSchema.validate< + Partial & { meta: string }> + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + }).value.query + ).toEqual(''); + }); }); describe('update signals schema', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index b4c5a8df981ad..fa773b684eb5d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -72,7 +72,14 @@ export const createSignalsSchema = Joi.object({ interval: interval.default('5m'), query: Joi.when('type', { is: 'query', - then: query.required(), + then: Joi.when('filters', { + is: Joi.exist(), + then: query + .optional() + .allow('') + .default(''), + otherwise: Joi.required(), + }), otherwise: Joi.when('type', { is: 'saved_query', then: query.optional(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json new file mode 100644 index 0000000000000..c136c9b0fe808 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json @@ -0,0 +1,53 @@ +{ + "rule_id": "filters-with-empty-query", + "risk_score": 7, + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-24h", + "to": "now", + "output_index": ".siem-signals", + "language": "lucene", + "query": "", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": "custom label here", + "disabled": false, + "key": "host.name", + "negate": false, + "params": { + "query": "siem-windows" + }, + "type": "phrase" + }, + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + }, + "meta": { + "type": "exists", + "disabled": false, + "negate": false, + "alias": "has a hostname", + "key": "host.hostname", + "value": "exists" + }, + "$state": { + "store": "appState" + } + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json new file mode 100644 index 0000000000000..5b69fced90daf --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json @@ -0,0 +1,52 @@ +{ + "rule_id": "filters-without-query", + "risk_score": 7, + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-24h", + "to": "now", + "output_index": ".siem-signals", + "language": "lucene", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": "custom label here", + "disabled": false, + "key": "host.name", + "negate": false, + "params": { + "query": "siem-windows" + }, + "type": "phrase" + }, + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + }, + "meta": { + "type": "exists", + "disabled": false, + "negate": false, + "alias": "has a hostname", + "key": "host.hostname", + "value": "exists" + }, + "$state": { + "store": "appState" + } + } + ] +} From 1034bb4819c53e3c38593ff65181c434cf2aef12 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 21 Nov 2019 20:06:08 -0700 Subject: [PATCH 015/128] [Reporting/np-k8] Remove several oncePerServer usages (#50997) * [Reporting/np-k8] Remove several oncePerServer usages * ts fixes 1 * ts fixes 2 * more ts fixes * more ts fixes * more ts fixes * ts simplification * improve ts * remove any type for jobParams and define JobParamsSavedObject and JobParamsUrl * ts simplification * Fix ts * ts simplification * fix ts * bug fix * align with joels pr --- .../reporting/common/get_absolute_url.ts | 5 +-- .../export_types/csv/server/create_job.ts | 13 ++---- .../export_types/csv/server/execute_job.js | 6 +-- .../server/create_job/create_job.ts | 14 ++---- .../server/execute_job.ts | 33 +++++++------- .../server/lib/get_job_params_from_request.ts | 2 +- .../csv_from_savedobject/types.d.ts | 3 +- .../png/server/create_job/index.ts | 18 ++------ .../png/server/execute_job/index.js | 6 +-- .../png/server/lib/generate_png.ts | 5 +-- .../printable_pdf/server/create_job/index.js | 6 +-- .../printable_pdf/server/execute_job/index.js | 6 +-- .../printable_pdf/server/lib/generate_pdf.ts | 5 +-- .../reporting/server/lib/create_queue.ts | 5 +-- .../reporting/server/lib/create_worker.ts | 5 +-- .../reporting/server/lib/enqueue_job.ts | 10 ++--- .../plugins/reporting/server/lib/get_user.js | 6 +-- .../reporting/server/lib/jobs_query.js | 5 +-- .../generate_from_savedobject_immediate.ts | 8 +++- .../server/routes/lib/get_document_payload.ts | 5 +-- .../server/routes/lib/job_response_handler.js | 5 +-- x-pack/legacy/plugins/reporting/types.d.ts | 44 ++++++++++++++----- 22 files changed, 92 insertions(+), 123 deletions(-) diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts b/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts index 79c5a274d8c77..1d34189abcb24 100644 --- a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts +++ b/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts @@ -5,10 +5,9 @@ */ import url from 'url'; -import { oncePerServer } from '../server/lib/once_per_server'; import { ServerFacade } from '../types'; -function getAbsoluteUrlFn(server: ServerFacade) { +export function getAbsoluteUrlFactory(server: ServerFacade) { const config = server.config(); return function getAbsoluteUrl({ @@ -27,5 +26,3 @@ function getAbsoluteUrlFn(server: ServerFacade) { }); }; } - -export const getAbsoluteUrlFactory = oncePerServer(getAbsoluteUrlFn); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts index f9542279f52d9..7cfed0d09a4fb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { oncePerServer } from '../../../server/lib/once_per_server'; import { cryptoFactory } from '../../../server/lib/crypto'; -import { ConditionalHeaders, CreateJobFactory, ServerFacade, RequestFacade } from '../../../types'; -import { JobParamsDiscoverCsv, ESQueueCreateJobFnDiscoverCsv } from '../types'; +import { ConditionalHeaders, ServerFacade, RequestFacade } from '../../../types'; +import { JobParamsDiscoverCsv } from '../types'; -function createJobFn(server: ServerFacade) { +export const createJobFactory = function createJobFn(server: ServerFacade) { const crypto = cryptoFactory(server); return async function createJob( @@ -32,8 +31,4 @@ function createJobFn(server: ServerFacade) { ...jobParams, }; }; -} - -export const createJobFactory: CreateJobFactory = oncePerServer( - createJobFn as (server: ServerFacade) => ESQueueCreateJobFnDiscoverCsv -); +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.js index ff49daced4a65..19656a94c6a46 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.js @@ -5,12 +5,12 @@ */ import { CSV_JOB_TYPE, PLUGIN_ID } from '../../../common/constants'; -import { cryptoFactory, oncePerServer, LevelLogger } from '../../../server/lib'; +import { cryptoFactory, LevelLogger } from '../../../server/lib'; import { createGenerateCsv } from './lib/generate_csv'; import { fieldFormatMapFactory } from './lib/field_format_map'; import { i18n } from '@kbn/i18n'; -function executeJobFn(server) { +export function executeJobFactory(server) { const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); const crypto = cryptoFactory(server); const config = server.config(); @@ -126,5 +126,3 @@ function executeJobFn(server) { }; }; } - -export const executeJobFactory = oncePerServer(executeJobFn); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts index 0bcfc6f1ca07c..a3e531d16a2e6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts @@ -7,8 +7,8 @@ import { notFound, notImplemented } from 'boom'; import { get } from 'lodash'; import { PLUGIN_ID, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; -import { cryptoFactory, LevelLogger, oncePerServer } from '../../../../server/lib'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { cryptoFactory, LevelLogger } from '../../../../server/lib'; +import { ImmediateCreateJobFn, ServerFacade, RequestFacade } from '../../../../types'; import { SavedObject, SavedObjectServiceError, @@ -27,13 +27,7 @@ interface VisData { panel: SearchPanel; } -type CreateJobFn = ( - jobParams: JobParamsPanelCsv, - headers: any, - req: RequestFacade -) => Promise; - -function createJobFn(server: ServerFacade): CreateJobFn { +export function createJobFactory(server: ServerFacade): ImmediateCreateJobFn { const crypto = cryptoFactory(server); const logger = LevelLogger.createForServer(server, [ PLUGIN_ID, @@ -100,5 +94,3 @@ function createJobFn(server: ServerFacade): CreateJobFn { }; }; } - -export const createJobFactory = oncePerServer(createJobFn); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts index 9d4bcf1e4b27a..d2284b73c7673 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts @@ -5,11 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { cryptoFactory, LevelLogger, oncePerServer } from '../../../server/lib'; +import { cryptoFactory, LevelLogger } from '../../../server/lib'; import { + ImmediateExecuteFn, JobDocOutputExecuted, ServerFacade, - ExecuteImmediateJobFactory, RequestFacade, } from '../../../types'; import { @@ -17,16 +17,16 @@ import { CSV_FROM_SAVEDOBJECT_JOB_TYPE, PLUGIN_ID, } from '../../../common/constants'; -import { CsvResultFromSearch, JobDocPayloadPanelCsv, FakeRequest } from '../types'; +import { + CsvResultFromSearch, + JobParamsPanelCsv, + SearchPanel, + JobDocPayloadPanelCsv, + FakeRequest, +} from '../types'; import { createGenerateCsv } from './lib'; -type ExecuteJobFn = ( - jobId: string | null, - job: JobDocPayloadPanelCsv, - realRequest?: RequestFacade -) => Promise; - -function executeJobFactoryFn(server: ServerFacade): ExecuteJobFn { +export function executeJobFactory(server: ServerFacade): ImmediateExecuteFn { const crypto = cryptoFactory(server); const logger = LevelLogger.createForServer(server, [ PLUGIN_ID, @@ -45,7 +45,14 @@ function executeJobFactoryFn(server: ServerFacade): ExecuteJobFn { const jobLogger = logger.clone([jobId === null ? 'immediate' : jobId]); const { jobParams } = job; - const { isImmediate, panel, visType } = jobParams; + const { isImmediate, panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel }; + + if (!panel) { + i18n.translate( + 'xpack.reporting.exportTypes.csv_from_savedobject.executeJob.failedToAccessPanel', + { defaultMessage: 'Failed to access panel metadata for job execution' } + ); + } jobLogger.debug(`Execute job generating [${visType}] csv`); @@ -112,7 +119,3 @@ function executeJobFactoryFn(server: ServerFacade): ExecuteJobFn { }; }; } - -export const executeJobFactory: ExecuteImmediateJobFactory = oncePerServer( - executeJobFactoryFn as ExecuteImmediateJobFactory -); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts index 774e430d593cd..8e5440b700d1e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts @@ -10,7 +10,7 @@ import { JobParamsPostPayloadPanelCsv, JobParamsPanelCsv } from '../../types'; export function getJobParamsFromRequest( request: RequestFacade, opts: { isImmediate: boolean } -): Partial { +): JobParamsPanelCsv { const { savedObjectType, savedObjectId } = request.params; const { timerange, state } = request.payload as JobParamsPostPayloadPanelCsv; const post = timerange || state ? { timerange, state } : undefined; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts index 72caf551d2a3d..a90b7e713e3ef 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/types.d.ts @@ -20,14 +20,13 @@ export interface JobParamsPanelCsv { savedObjectType: string; savedObjectId: string; isImmediate: boolean; - panel: SearchPanel; + panel?: SearchPanel; post?: JobParamsPostPayloadPanelCsv; visType?: string; } export interface JobDocPayloadPanelCsv extends JobDocPayload { type: string | null; - objects: null; jobParams: JobParamsPanelCsv; } diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts index f1008a4866fd7..a5b6c37b20c18 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ServerFacade, - RequestFacade, - ConditionalHeaders, - CreateJobFactory, -} from '../../../../types'; +import { ServerFacade, RequestFacade, ConditionalHeaders } from '../../../../types'; import { validateUrls } from '../../../../common/validate_urls'; import { cryptoFactory } from '../../../../server/lib/crypto'; -import { oncePerServer } from '../../../../server/lib/once_per_server'; -import { JobParamsPNG, ESQueueCreateJobFnPNG } from '../../types'; +import { JobParamsPNG } from '../../types'; -function createJobFn(server: ServerFacade) { +export const createJobFactory = function createJobFn(server: ServerFacade) { const crypto = cryptoFactory(server); return async function createJob( @@ -38,8 +32,4 @@ function createJobFn(server: ServerFacade) { forceNow: new Date().toISOString(), }; }; -} - -export const createJobFactory: CreateJobFactory = oncePerServer( - createJobFn as (server: ServerFacade) => ESQueueCreateJobFnPNG -); +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.js index 2642d88983ecf..7c6ca8520c51e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.js @@ -8,7 +8,7 @@ import * as Rx from 'rxjs'; import { i18n } from '@kbn/i18n'; import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; import { PLUGIN_ID, PNG_JOB_TYPE } from '../../../../common/constants'; -import { LevelLogger, oncePerServer } from '../../../../server/lib'; +import { LevelLogger } from '../../../../server/lib'; import { generatePngObservableFactory } from '../lib/generate_png'; import { decryptJobHeaders, @@ -17,7 +17,7 @@ import { getFullUrls, } from '../../../common/execute_job/'; -function executeJobFn(server) { +export function executeJobFactory(server) { const generatePngObservable = generatePngObservableFactory(server); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PNG_JOB_TYPE, 'execute']); @@ -67,5 +67,3 @@ function executeJobFn(server) { return process$.pipe(takeUntil(stop$)).toPromise(); }; } - -export const executeJobFactory = oncePerServer(executeJobFn); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index 5980f1884ab2f..81b37ecf74f73 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -8,7 +8,6 @@ import * as Rx from 'rxjs'; import { toArray, mergeMap } from 'rxjs/operators'; import { LevelLogger } from '../../../../server/lib'; import { ServerFacade, ConditionalHeaders } from '../../../../types'; -import { oncePerServer } from '../../../../server/lib/once_per_server'; import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { PreserveLayout } from '../../../common/layouts/preserve_layout'; import { LayoutParams } from '../../../common/layouts/layout'; @@ -21,7 +20,7 @@ interface UrlScreenshot { screenshots: ScreenshotData[]; } -function generatePngObservableFn(server: ServerFacade) { +export function generatePngObservableFactory(server: ServerFacade) { const screenshotsObservable = screenshotsObservableFactory(server); const captureConcurrency = 1; @@ -68,5 +67,3 @@ function generatePngObservableFn(server: ServerFacade) { ); }; } - -export const generatePngObservableFactory = oncePerServer(generatePngObservableFn); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.js index 1fb980396c8d3..d6c6eb7eb2007 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.js @@ -6,11 +6,11 @@ import { PLUGIN_ID, PDF_JOB_TYPE } from '../../../../common/constants'; import { validateUrls } from '../../../../common/validate_urls'; -import { LevelLogger, oncePerServer } from '../../../../server/lib'; +import { LevelLogger } from '../../../../server/lib'; import { cryptoFactory } from '../../../../server/lib/crypto'; import { compatibilityShimFactory } from './compatibility_shim'; -function createJobFactoryFn(server) { +export function createJobFactory(server) { const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PDF_JOB_TYPE, 'create']); const compatibilityShim = compatibilityShimFactory(server, logger); const crypto = cryptoFactory(server); @@ -36,5 +36,3 @@ function createJobFactoryFn(server) { }; }); } - -export const createJobFactory = oncePerServer(createJobFactoryFn); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js index d340a09728ca7..d55ee95ff215b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js @@ -8,7 +8,7 @@ import * as Rx from 'rxjs'; import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { PLUGIN_ID, PDF_JOB_TYPE } from '../../../../common/constants'; -import { LevelLogger, oncePerServer } from '../../../../server/lib'; +import { LevelLogger } from '../../../../server/lib'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { decryptJobHeaders, @@ -18,7 +18,7 @@ import { getCustomLogo, } from '../../../common/execute_job/'; -function executeJobFn(server) { +export function executeJobFactory(server) { const generatePdfObservable = generatePdfObservableFactory(server); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PDF_JOB_TYPE, 'execute']); @@ -71,5 +71,3 @@ function executeJobFn(server) { return process$.pipe(takeUntil(stop$)).toPromise(); }; } - -export const executeJobFactory = oncePerServer(executeJobFn); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index 0d2243acfef9b..52e867cc471ce 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -11,7 +11,6 @@ import { LevelLogger } from '../../../../server/lib'; import { ServerFacade, ConditionalHeaders } from '../../../../types'; // @ts-ignore untyped module import { pdf } from './pdf'; -import { oncePerServer } from '../../../../server/lib/once_per_server'; import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { createLayout } from '../../../common/layouts'; import { TimeRange } from '../../../common/lib/screenshots/types'; @@ -38,7 +37,7 @@ const getTimeRange = (urlScreenshots: UrlScreenshot[]) => { return null; }; -function generatePdfObservableFn(server: ServerFacade) { +export function generatePdfObservableFactory(server: ServerFacade) { const screenshotsObservable = screenshotsObservableFactory(server); const captureConcurrency = 1; @@ -87,5 +86,3 @@ function generatePdfObservableFn(server: ServerFacade) { ); }; } - -export const generatePdfObservableFactory = oncePerServer(generatePdfObservableFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index a7e81093c136a..174c6d587e523 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -9,12 +9,11 @@ import { ServerFacade, QueueConfig } from '../../types'; // @ts-ignore import { Esqueue } from './esqueue'; import { createWorkerFactory } from './create_worker'; -import { oncePerServer } from './once_per_server'; import { LevelLogger } from './level_logger'; // @ts-ignore import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed -function createQueueFn(server: ServerFacade): Esqueue { +export function createQueueFactory(server: ServerFacade): Esqueue { const queueConfig: QueueConfig = server.config().get('xpack.reporting.queue'); const index = server.config().get('xpack.reporting.index'); @@ -45,5 +44,3 @@ function createQueueFn(server: ServerFacade): Esqueue { return queue; } - -export const createQueueFactory = oncePerServer(createQueueFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 1cfc967cb31d1..0a86f9d1d4ff5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -21,9 +21,8 @@ import { // @ts-ignore untyped dependency import { events as esqueueEvents } from './esqueue'; import { LevelLogger } from './level_logger'; -import { oncePerServer } from './once_per_server'; -function createWorkerFn(server: ServerFacade) { +export function createWorkerFactory(server: ServerFacade) { const config = server.config(); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'queue-worker']); const queueConfig: QueueConfig = config.get('xpack.reporting.queue'); @@ -79,5 +78,3 @@ function createWorkerFn(server: ServerFacade) { }); }; } - -export const createWorkerFactory = oncePerServer(createWorkerFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index c264c0ca7e0eb..9b1d7992283a5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -7,8 +7,8 @@ import { get } from 'lodash'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; -import { oncePerServer } from './once_per_server'; import { + Job, ServerFacade, RequestFacade, Logger, @@ -24,7 +24,7 @@ interface ConfirmedJob { _primary_term: number; } -function enqueueJobFn(server: ServerFacade) { +export function enqueueJobFactory(server: ServerFacade) { const config = server.config(); const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); const browserType = captureConfig.browser.type; @@ -37,9 +37,9 @@ function enqueueJobFn(server: ServerFacade) { exportTypeId: string, jobParams: object, user: string, - headers: ConditionalHeaders, + headers: ConditionalHeaders['headers'], request: RequestFacade - ) { + ): Promise { const logger = parentLogger.clone(['queue-job']); const exportType = exportTypesRegistry.getById(exportTypeId); const createJob = exportType.createJobFactory(server); @@ -65,5 +65,3 @@ function enqueueJobFn(server: ServerFacade) { }); }; } - -export const enqueueJobFactory = oncePerServer(enqueueJobFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.js b/x-pack/legacy/plugins/reporting/server/lib/get_user.js index 2c4f3bcb2dd36..04c9516cb99d4 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.js +++ b/x-pack/legacy/plugins/reporting/server/lib/get_user.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { oncePerServer } from './once_per_server'; - -function getUserFn(server) { +export function getUserFactory(server) { return async request => { if (!server.plugins.security) { return null; @@ -20,5 +18,3 @@ function getUserFn(server) { } }; } - -export const getUserFactory = oncePerServer(getUserFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.js b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.js index e4f501e2c9518..fef9a529f7a2c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.js +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.js @@ -5,11 +5,10 @@ */ import { get } from 'lodash'; -import { oncePerServer } from './once_per_server'; const defaultSize = 10; -function jobsQueryFn(server) { +export function jobsQueryFactory(server) { const index = server.config().get('xpack.reporting.index'); const { callWithInternalUser, errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); @@ -138,5 +137,3 @@ function jobsQueryFn(server) { } }; } - -export const jobsQueryFactory = oncePerServer(jobsQueryFn); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 557f7c3702038..2303fddf555e0 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -12,10 +12,10 @@ import { ResponseFacade, ReportingResponseToolkit, Logger, - JobDocPayload, JobIDForImmediate, JobDocOutputExecuted, } from '../../types'; +import { JobDocPayloadPanelCsv } from '../../export_types/csv_from_savedobject/types'; import { getRouteOptionsCsv } from './lib/route_config_factories'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; @@ -48,7 +48,11 @@ export function registerGenerateCsvFromSavedObjectImmediate( const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); const createJobFn = createJobFactory(server); const executeJobFn = executeJobFactory(server); - const jobDocPayload: JobDocPayload = await createJobFn(jobParams, request.headers, request); + const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( + jobParams, + request.headers, + request + ); const { content_type: jobOutputContentType, content: jobOutputContent, diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index d3e9981a62b6e..9952cbb980778 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -13,7 +13,6 @@ import { JobDocExecuted, JobDocOutputExecuted, } from '../../../types'; -import { oncePerServer } from '../../lib/once_per_server'; import { CSV_JOB_TYPE } from '../../../common/constants'; interface ICustomHeaders { @@ -39,7 +38,7 @@ const getReportingHeaders = (output: JobDocOutputExecuted, exportType: ExportTyp return metaDataHeaders; }; -function getDocumentPayloadFn(server: ServerFacade) { +export function getDocumentPayloadFactory(server: ServerFacade) { const exportTypesRegistry = server.plugins.reporting!.exportTypesRegistry; function encodeContent(content: string | null, exportType: ExportTypeDefinition) { @@ -105,5 +104,3 @@ function getDocumentPayloadFn(server: ServerFacade) { return getIncomplete(status); }; } - -export const getDocumentPayloadFactory = oncePerServer(getDocumentPayloadFn); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js index 75bffcafc5c33..758c50816c381 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js @@ -5,12 +5,11 @@ */ import boom from 'boom'; -import { oncePerServer } from '../../lib/once_per_server'; import { jobsQueryFactory } from '../../lib/jobs_query'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { getDocumentPayloadFactory } from './get_document_payload'; -function jobResponseHandlerFn(server) { +export function jobResponseHandlerFactory(server) { const jobsQuery = jobsQueryFactory(server); const getDocumentPayload = getDocumentPayloadFactory(server); @@ -45,5 +44,3 @@ function jobResponseHandlerFn(server) { }); }; } - -export const jobResponseHandlerFactory = oncePerServer(jobResponseHandlerFn); diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 6d2808c5b560d..7d05811ef4aa6 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -16,7 +16,12 @@ import { CancellationToken } from './common/cancellation_token'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; -type Job = EventEmitter & { id: string }; +export type Job = EventEmitter & { + id: string; + toJSON: () => { + id: string; + }; +}; export interface ReportingPlugin { queue: { @@ -193,9 +198,10 @@ export interface JobParamPostPayload { export interface JobDocPayload { headers?: Record; - jobParams: object; + jobParams: any; title: string; type: string | null; + objects?: null | object[]; } export interface JobSource { @@ -245,11 +251,29 @@ export interface ESQueueWorker { on: (event: string, handler: any) => void; } +type JobParamsUrl = object; + +interface JobParamsSavedObject { + savedObjectType: string; + savedObjectId: string; + isImmediate: boolean; +} + export type ESQueueCreateJobFn = ( - jobParams: object, - headers: ConditionalHeaders, + jobParams: JobParamsSavedObject | JobParamsUrl, + headers: Record, request: RequestFacade -) => Promise; +) => Promise; + +export type ImmediateCreateJobFn = ( + jobParams: any, + headers: Record, + req: RequestFacade +) => Promise<{ + type: string | null; + title: string; + jobParams: any; +}>; export type ESQueueWorkerExecuteFn = ( jobId: string, @@ -258,9 +282,10 @@ export type ESQueueWorkerExecuteFn = ( ) => void; export type JobIDForImmediate = null; + export type ImmediateExecuteFn = ( jobId: JobIDForImmediate, - jobDocPayload: JobDocPayload, + job: JobDocPayload, request: RequestFacade ) => Promise; @@ -279,9 +304,8 @@ export interface ESQueueInstance { ) => ESQueueWorker; } -export type CreateJobFactory = (server: ServerFacade) => ESQueueCreateJobFn; -export type ExecuteJobFactory = (server: ServerFacade) => ESQueueWorkerExecuteFn; -export type ExecuteImmediateJobFactory = (server: ServerFacade) => ImmediateExecuteFn; +export type CreateJobFactory = (server: ServerFacade) => ESQueueCreateJobFn | ImmediateCreateJobFn; +export type ExecuteJobFactory = (server: ServerFacade) => ESQueueWorkerExecuteFn | ImmediateExecuteFn; // prettier-ignore export interface ExportTypeDefinition { id: string; @@ -290,7 +314,7 @@ export interface ExportTypeDefinition { jobContentEncoding?: string; jobContentExtension: string; createJobFactory: CreateJobFactory; - executeJobFactory: ExecuteJobFactory | ExecuteImmediateJobFactory; + executeJobFactory: ExecuteJobFactory; validLicenses: string[]; } From 5d29ede6ce74ca4fb3946d657735c656657a761b Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 21 Nov 2019 21:11:57 -0600 Subject: [PATCH 016/128] [APM] Fix typo in serviceMapEnabled config value (#51382) --- x-pack/legacy/plugins/apm/server/routes/services.ts | 2 +- x-pack/plugins/apm/server/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/apm/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts index ea9fdaa2a4aa4..91495bb96b032 100644 --- a/x-pack/legacy/plugins/apm/server/routes/services.ts +++ b/x-pack/legacy/plugins/apm/server/routes/services.ts @@ -92,7 +92,7 @@ export const serviceMapRoute = createRoute(() => ({ query: rangeRt }, handler: async ({ context }) => { - if (context.config['xpack.apm.servicemapEnabled']) { + if (context.config['xpack.apm.serviceMapEnabled']) { return getServiceMap(); } return new Boom('Not found', { statusCode: 404 }); diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 6e1257bc4d1c4..b66850ff569cb 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -11,7 +11,7 @@ import { APMPlugin } from './plugin'; export const config = { schema: schema.object({ - servicemapEnabled: schema.boolean({ defaultValue: false }), + serviceMapEnabled: schema.boolean({ defaultValue: false }), autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), 'ui.transactionGroupBucketSize': schema.number({ defaultValue: 100 }), 'ui.maxTraceItems': schema.number({ defaultValue: 1000 }), @@ -29,7 +29,7 @@ export function mergeConfigs(apmOssConfig: APMOSSConfig, apmConfig: APMXPackConf 'apm_oss.sourcemapIndices': apmOssConfig.sourcemapIndices, 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices, 'apm_oss.indexPattern': apmOssConfig.indexPattern, - 'xpack.apm.servicemapEnabled': apmConfig.servicemapEnabled, + 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled, 'xpack.apm.ui.maxTraceItems': apmConfig['ui.maxTraceItems'], 'xpack.apm.ui.transactionGroupBucketSize': apmConfig['ui.transactionGroupBucketSize'], 'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern, From 0ce30b5ce1be483d61d2f647641d611ee63720bf Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Fri, 22 Nov 2019 10:48:54 +0100 Subject: [PATCH 017/128] Retry authentication and other connection failures in Saved Object migrations (#51324) * Retry authentication and other connection failures in migrations * Log migration ES connection errors once, retry every 2.5s * Test migrations es connection error logging * retry on AuthorizationExceptions during migration * Set delay to 1 for tests --- .../elasticsearch/retry_call_cluster.test.ts | 84 +++++++++++++++++-- .../elasticsearch/retry_call_cluster.ts | 56 +++++++++++++ .../saved_objects_service.test.ts | 2 +- .../saved_objects/saved_objects_service.ts | 13 ++- 4 files changed, 146 insertions(+), 9 deletions(-) diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts index 0de10e8fb4f77..275bda17ab92f 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.test.ts @@ -18,17 +18,17 @@ */ import * as legacyElasticsearch from 'elasticsearch'; -import { retryCallCluster } from './retry_call_cluster'; +import { retryCallCluster, migrationsRetryCallCluster } from './retry_call_cluster'; +import { loggingServiceMock } from '../logging/logging_service.mock'; describe('retryCallCluster', () => { - it('retries ES API calls that rejects with NoConnection errors', () => { + it('retries ES API calls that rejects with NoConnections', () => { expect.assertions(1); const callEsApi = jest.fn(); let i = 0; + const ErrorConstructor = legacyElasticsearch.errors.NoConnections; callEsApi.mockImplementation(() => { - return i++ <= 2 - ? Promise.reject(new legacyElasticsearch.errors.NoConnections()) - : Promise.resolve('success'); + return i++ <= 2 ? Promise.reject(new ErrorConstructor()) : Promise.resolve('success'); }); const retried = retryCallCluster(callEsApi); return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); @@ -57,3 +57,77 @@ describe('retryCallCluster', () => { return expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); }); }); + +describe('migrationsRetryCallCluster', () => { + const errors = [ + 'NoConnections', + 'ConnectionFault', + 'ServiceUnavailable', + 'RequestTimeout', + 'AuthenticationException', + 'AuthorizationException', + ]; + + const mockLogger = loggingServiceMock.create(); + + beforeEach(() => { + loggingServiceMock.clear(mockLogger); + }); + + errors.forEach(errorName => { + it('retries ES API calls that rejects with ' + errorName, () => { + expect.assertions(1); + const callEsApi = jest.fn(); + let i = 0; + const ErrorConstructor = (legacyElasticsearch.errors as any)[errorName]; + callEsApi.mockImplementation(() => { + return i++ <= 2 ? Promise.reject(new ErrorConstructor()) : Promise.resolve('success'); + }); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + }); + }); + + it('rejects when ES API calls reject with other errors', async () => { + expect.assertions(3); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + i++; + + return i === 1 + ? Promise.reject(new Error('unknown error')) + : i === 2 + ? Promise.resolve('success') + : i === 3 || i === 4 + ? Promise.reject(new legacyElasticsearch.errors.NoConnections()) + : i === 5 + ? Promise.reject(new Error('unknown error')) + : null; + }); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + await expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + await expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + return expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + }); + + it('logs only once for each unique error message', async () => { + const callEsApi = jest.fn(); + callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.NoConnections()); + callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.NoConnections()); + callEsApi.mockRejectedValueOnce(new legacyElasticsearch.errors.AuthenticationException()); + callEsApi.mockResolvedValueOnce('done'); + const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1); + await retried('endpoint'); + expect(loggingServiceMock.collect(mockLogger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Unable to connect to Elasticsearch. Error: No Living connections", + ], + Array [ + "Unable to connect to Elasticsearch. Error: Authentication Exception", + ], + ] + `); + }); +}); diff --git a/src/core/server/elasticsearch/retry_call_cluster.ts b/src/core/server/elasticsearch/retry_call_cluster.ts index 2e4afa682ea75..89d7b88b1675a 100644 --- a/src/core/server/elasticsearch/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/retry_call_cluster.ts @@ -22,6 +22,62 @@ import { defer, throwError, iif, timer } from 'rxjs'; import * as legacyElasticsearch from 'elasticsearch'; import { CallAPIOptions } from '.'; +import { Logger } from '../logging'; + +const esErrors = legacyElasticsearch.errors; + +/** + * Retries the provided Elasticsearch API call when an error such as + * `AuthenticationException` `NoConnections`, `ConnectionFault`, + * `ServiceUnavailable` or `RequestTimeout` are encountered. The API call will + * be retried once a second, indefinitely, until a successful response or a + * different error is received. + * + * @param apiCaller + */ + +// TODO: Replace with APICaller from './scoped_cluster_client' once #46668 is merged +export function migrationsRetryCallCluster( + apiCaller: ( + endpoint: string, + clientParams: Record, + options?: CallAPIOptions + ) => Promise, + log: Logger, + delay: number = 2500 +) { + const previousErrors: string[] = []; + return (endpoint: string, clientParams: Record = {}, options?: CallAPIOptions) => { + return defer(() => apiCaller(endpoint, clientParams, options)) + .pipe( + retryWhen(error$ => + error$.pipe( + concatMap((error, i) => { + if (!previousErrors.includes(error.message)) { + log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); + previousErrors.push(error.message); + } + return iif( + () => { + return ( + error instanceof esErrors.NoConnections || + error instanceof esErrors.ConnectionFault || + error instanceof esErrors.ServiceUnavailable || + error instanceof esErrors.RequestTimeout || + error instanceof esErrors.AuthenticationException || + error instanceof esErrors.AuthorizationException + ); + }, + timer(delay), + throwError(error) + ); + }) + ) + ) + ) + .toPromise(); + }; +} /** * Retries the provided Elasticsearch API call when a `NoConnections` error is diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 07bb4342c754a..c31ad90011865 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -54,7 +54,7 @@ describe('SavedObjectsService', () => { legacy: { uiExports: { savedObjectMappings: [] }, pluginExtendedConfig: {} }, } as unknown) as SavedObjectsSetupDeps; - await soService.setup(coreSetup); + await soService.setup(coreSetup, 1); return expect((KibanaMigrator as jest.Mock).mock.calls[0][0].callCluster()).resolves.toMatch( 'success' diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 5ccb02414d043..43c3afa3ed639 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -33,7 +33,7 @@ import { CoreContext } from '../core_context'; import { LegacyServiceSetup } from '../legacy/legacy_service'; import { ElasticsearchServiceSetup } from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; -import { retryCallCluster } from '../elasticsearch/retry_call_cluster'; +import { retryCallCluster, migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster'; import { SavedObjectsConfigType } from './saved_objects_config'; import { KibanaRequest } from '../http'; import { Logger } from '..'; @@ -73,7 +73,10 @@ export class SavedObjectsService this.logger = coreContext.logger.get('savedobjects-service'); } - public async setup(coreSetup: SavedObjectsSetupDeps): Promise { + public async setup( + coreSetup: SavedObjectsSetupDeps, + migrationsRetryDelay?: number + ): Promise { this.logger.debug('Setting up SavedObjects service'); const { @@ -105,7 +108,11 @@ export class SavedObjectsService config: coreSetup.legacy.pluginExtendedConfig, savedObjectsConfig, kibanaConfig, - callCluster: retryCallCluster(adminClient.callAsInternalUser), + callCluster: migrationsRetryCallCluster( + adminClient.callAsInternalUser, + this.coreContext.logger.get('migrations'), + migrationsRetryDelay + ), })); const mappings = this.migrator.getActiveMappings(); From 67a3489a4b067ac3a7253b35211de0fdb016c9a7 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 22 Nov 2019 11:15:47 +0100 Subject: [PATCH 018/128] fixes toggle columns cypress tests (#51279) --- .../siem/cypress/integration/lib/timeline/helpers.ts | 2 +- .../cypress/integration/lib/timeline/selectors.ts | 3 +-- .../smoke_tests/timeline/toggle_column.spec.ts | 12 +++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts index 0bcd034a24cee..8fa1a03840e3b 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/helpers.ts @@ -47,5 +47,5 @@ export const assertAtLeastOneEventMatchesSearch = () => export const toggleFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT, { timeout: DEFAULT_TIMEOUT }) .first() - .click(); + .click({ force: true }); }; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts index 14f06b55fd5ee..7dc98072b52f8 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/timeline/selectors.ts @@ -32,8 +32,7 @@ export const SEARCH_OR_FILTER_CONTAINER = export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; /** Expands or collapses an event in the timeline */ -export const TOGGLE_TIMELINE_EXPAND_EVENT = - '[data-test-subj="timeline"] [data-test-subj="expand-event"]'; +export const TOGGLE_TIMELINE_EXPAND_EVENT = '[data-test-subj="expand-event"]'; /** The body of the timeline flyout */ export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts index 3f6ed86f29285..8c2902fd804ac 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts @@ -23,7 +23,7 @@ describe('toggle column in timeline', () => { const timestampField = '@timestamp'; const idField = '_id'; - it.skip('displays a checked Toggle field checkbox for `@timestamp`, a default timeline column', () => { + it('displays a checked Toggle field checkbox for `@timestamp`, a default timeline column', () => { populateTimeline(); toggleFirstTimelineEventDetails(); @@ -39,7 +39,7 @@ describe('toggle column in timeline', () => { ); }); - it.skip('removes the @timestamp field from the timeline when the user un-checks the toggle', () => { + it('removes the @timestamp field from the timeline when the user un-checks the toggle', () => { populateTimeline(); toggleFirstTimelineEventDetails(); @@ -50,14 +50,14 @@ describe('toggle column in timeline', () => { cy.get( `[data-test-subj="timeline"] [data-test-subj="toggle-field-${timestampField}"]` - ).uncheck(); + ).uncheck({ force: true }); cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${timestampField}"]`).should( 'not.exist' ); }); - it.skip('adds the _id field to the timeline when the user checks the field', () => { + it('adds the _id field to the timeline when the user checks the field', () => { populateTimeline(); toggleFirstTimelineEventDetails(); @@ -66,7 +66,9 @@ describe('toggle column in timeline', () => { 'not.exist' ); - cy.get(`[data-test-subj="timeline"] [data-test-subj="toggle-field-${idField}"]`).check(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="toggle-field-${idField}"]`).check({ + force: true, + }); cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${idField}"]`).should('exist'); }); From b5b81791a1d1125e6de138f03cbfdef51b594490 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Fri, 22 Nov 2019 14:02:03 +0300 Subject: [PATCH 019/128] Use NP registry instead of ui/registry/field_formats - cleanup (#51419) Cleanup after merging #48108 --- src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts b/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts index a466b9f852607..c24dda180ea94 100644 --- a/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts +++ b/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts @@ -23,9 +23,6 @@ import { AggType, AggTypeConfig } from '../agg_type'; import { AggParamType } from '../param_types/agg'; import { AggConfig } from '../agg_config'; import { METRIC_TYPES } from './metric_agg_types'; - -// @ts-ignore -import { fieldFormats } from '../../registry/field_formats'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; export type IMetricAggConfig = AggConfig; From c9c4f7930d87932d59b5672b428f59c37cefe0a8 Mon Sep 17 00:00:00 2001 From: Court Ewing Date: Fri, 22 Nov 2019 07:39:03 -0500 Subject: [PATCH 020/128] Remove all code associated with node-ctags (#51405) This was only used by the code plugin, which was removed in 7.5.0. --- src/dev/build/build_distributables.js | 2 -- src/dev/build/tasks/clean_tasks.js | 39 --------------------------- src/dev/license_checker/config.ts | 2 -- 3 files changed, 43 deletions(-) diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js index eeffc2380f38b..d78a0654646c3 100644 --- a/src/dev/build/build_distributables.js +++ b/src/dev/build/build_distributables.js @@ -30,7 +30,6 @@ import { CleanTypescriptTask, CleanNodeBuildsTask, CleanTask, - CleanCtagBuildTask, CopySourceTask, CreateArchivesSourcesTask, CreateArchivesTask, @@ -133,7 +132,6 @@ export async function buildDistributables(options) { await run(CleanExtraBinScriptsTask); await run(CleanExtraBrowsersTask); await run(CleanNodeBuildsTask); - await run(CleanCtagBuildTask); await run(PathLengthTask); diff --git a/src/dev/build/tasks/clean_tasks.js b/src/dev/build/tasks/clean_tasks.js index c33dfe6262128..b23db67cc1b07 100644 --- a/src/dev/build/tasks/clean_tasks.js +++ b/src/dev/build/tasks/clean_tasks.js @@ -20,9 +20,6 @@ import minimatch from 'minimatch'; import { deleteAll, deleteEmptyFolders, scanDelete } from '../lib'; -import { resolve } from 'path'; - -const RELATIVE_CTAGS_BUILD_DIR = 'node_modules/@elastic/node-ctags/ctags/build'; export const CleanTask = { global: true, @@ -256,39 +253,3 @@ export const CleanEmptyFoldersTask = { ]); }, }; - -export const CleanCtagBuildTask = { - description: 'Cleaning extra platform-specific files from @elastic/node-ctag build dir', - - async run(config, log, build) { - const getPlatformId = platform => { - if (platform.isWindows()) { - return 'win32'; - } else if (platform.isLinux()) { - return 'linux'; - } else if (platform.isMac()) { - return 'darwin'; - } - }; - - await Promise.all( - config.getTargetPlatforms().map(async platform => { - if (build.isOss()) { - return; - } - - const ctagsBuildDir = build.resolvePathForPlatform(platform, RELATIVE_CTAGS_BUILD_DIR); - await deleteAll( - [ - resolve(ctagsBuildDir, '*'), - `!${resolve( - ctagsBuildDir, - `ctags-node-v${process.versions.modules}-${getPlatformId(platform)}-x64` - )}`, - ], - log - ); - }) - ); - }, -}; diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index fbd16d95ded1c..a4aa3474c0762 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -105,6 +105,4 @@ export const LICENSE_OVERRIDES = { // TODO can be removed once we upgrade the use of walk dependency past or equal to v2.3.14 'walk@2.3.9': ['MIT'], - - '@elastic/node-ctags@1.0.2': ['Nuclide software'], }; From bbe287f05fdbc2a5c3406586e765eb71826a3263 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 22 Nov 2019 14:05:00 +0100 Subject: [PATCH 021/128] docs: improve CONTRIBUTING docs for how to run Kibana tests (#51285) --- CONTRIBUTING.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64a1dd0526d58..e2a8459c2b01a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -399,13 +399,19 @@ Test runner arguments: - `[test path]` is the relative path to the test file. Examples: - - Run the entire elasticsearch_service test suite with yarn: - `node scripts/jest src/core/server/elasticsearch/elasticsearch_service.test.ts` - - Run the jest test case whose description matches 'stops both admin and data clients': - `node scripts/jest -t 'stops both admin and data clients' src/core/server/elasticsearch/elasticsearch_service.test.ts` + - Run the entire elasticsearch_service test suite: + ``` + node scripts/jest src/core/server/elasticsearch/elasticsearch_service.test.ts + ``` + - Run the jest test case whose description matches `stops both admin and data clients`: + ``` + node scripts/jest -t 'stops both admin and data clients' src/core/server/elasticsearch/elasticsearch_service.test.ts + ``` - Run the api integration test case whose description matches the given string: - `node scripts/functional_tests_server --config test/api_integration/config.js` - `node scripts/functional_test_runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets'` + ``` + node scripts/functional_tests_server --config test/api_integration/config.js + node scripts/functional_test_runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets' + ``` ### Debugging Unit Tests From ed8b822c8f3aef6d180e20e96fba4f80d1151878 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Fri, 22 Nov 2019 14:46:57 +0100 Subject: [PATCH 022/128] [ML] Add ownership of transform functional test files to ml-ui (#51418) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fbdf0cb5e5434..bd73e60d1c914 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -40,6 +40,9 @@ # ML team owns the transform plugin, ES team added here for visibility # because the plugin lives in Kibana's Elasticsearch management section. /x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui +/x-pack/test/functional/apps/transform/ @elastic/ml-ui +/x-pack/test/functional/services/transform_ui/ @elastic/ml-ui +/x-pack/test/functional/services/transform.ts @elastic/ml-ui # Operations /renovate.json5 @elastic/kibana-operations From b6d4e3a590661044e26303848b966029391198c4 Mon Sep 17 00:00:00 2001 From: kaiyan-sheng Date: Fri, 22 Nov 2019 07:00:36 -0700 Subject: [PATCH 023/128] Add AWS logs filebeat module to Kibana home (#51236) * Add AWS logs filebeat module to Kibana home * Fix merge conflict * Fix Cloudwatch Logs to AWS Cloudwatch logs --- .../aws_logs/screenshot.png | Bin 0 -> 588256 bytes .../kibana/server/tutorials/aws_logs/index.js | 64 ++++++++++++++++++ .../server/tutorials/cloudwatch_logs/index.js | 6 +- .../kibana/server/tutorials/register.js | 2 + 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_logs/screenshot.png create mode 100644 src/legacy/core_plugins/kibana/server/tutorials/aws_logs/index.js diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_logs/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_logs/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..fc895ceab20ba0928ee0ac71fbb93e88b1d50961 GIT binary patch literal 588256 zcmbrlbyOVBvj++Uf(5q_77fk#>K*``DCaBoX;ft`h^u8-@4d?z&$YaX4cED!>=M9 zQjzI%E%>nHHT## z<#U_h<@c516RqgM`{bk5GT2%;=7A7L%IhVW4bJcPid zqRwAwxni6yDBYr+HYjCjyeWEplzcH@{CkX$<6*T?=q2&_2&2Rdzgv&)p54;M?7Em2 zRddtYiF#;A4%9l>N-VISGdp~H!Dmw@iSL(5f|xMkP~2XK2RLh-ym$a!bO`sql>lDv zea}aF)jJ9Dd0d)GLKsfX<`!}e_sBqgK~{)^my~DG*}@U(2PD~5d)wSU#?mQl+_7yt z^o69dSYUNe3ko{6DPc3M zI(?fk&^Hd=V|F=DarASLG5OvuIeWR%oU3(5!mVo{)RIX`b%Pb!v3-uPZ|LY}>-lAg z2^=0@NAsJc_$$|2DNYD zvh<>Mi0HnKA8$U>7P(kp-?J~^k^<}r&Upmy)!l7R+5T1`m~EZE8EFMZ^T*5?Rx?&7%<7lZ zZk1a+{&1MU$O$ZgK5Pe3?^q>D5^2%EA(?!5I;qk)-h708;S(|ylec$~-G-mvz8=8d z3`g!0H=IwA8<83Dl%v|gEW~m`c0!zvAnf)YRH@O{LM=y43XAHeHm5}}Jd=wSpeb#;Ced1KPSsK20f>@6&I7fuLw|Dd{$&abIqAW$X zO8Cl@31|sl6F9+QU_SapOS!B30oDFrXusYaNF1;o&>*A*B*Lls=T*z($mA%mH}+Ub zup;)tD&`eD_vR$hh3&v>DeGL>r?9n%SF=OBL;or3 zo&YU4I9oDpz$}V7swnNPywfN`+Co|z2eTFWxLW2uZ@fUA$xc{mO#LTDrwaRuZS9A8 z-Ui7Am-<)r2*3I2nJqC6CtUblR9qSlGNuWq3iewMFPxhWvgUk#DjB=8OcCNm%0^-d zFm?zFgstdzFmMe~*NYsKs1*YAT{gCTDqXjW*Xgji7-kt&gm zL@sZA*g09ES@f-zO!zelq?@~q zJlopXW!iSE-q%Eh%g4&+$VZIo=s4*#+9V$9@s(zFEz`G|wQ)HCckFkMvLHXQ%!I~$ zD}36XxDcffbHp-xWAk#112z=~9~4Rz3>6MDD|wAGJ2EwSnt1(O9qh5%GVJfBXQntt zuc{B%bmLo=S{`< zt{boMJ-OWdE*;K6>(e_Cx36!LZe8z6ZYA#2?*$-pH;UI)SER_*7+NF~_@QWT(3b%2 z*(IYUY*s1nMWRI*0+8hKR*^KVS?W*W8brn zPy*SGM&OnLsu9#t^#d0JcSGF7Cn%C60nxXZ`@}6Q7Y+#)36!M*+AN;R&hnRoZ&y=l z-&!0U&EwT@ZLn{;JB*&+-tOJLI?qFW!nf6#?;7$|KF`hSr;c#KH_*v#q4fE=P!KK) zl2xSsgkyw_PUCGFR9gzl3mfUn40~@)dhL465KUvwU>*WhYG{ zbN-P|k#OhkjNZde<(2A9zONjrJYNd6Y>af(IGG8hNybju+4OC4(mrq)G|O0xiHn+_ z%9OguDizq>^NQkQz1A@vgFe8%H!Fi z3pfZgXSN%k8#c&XZR@e>UrC$*_az>ez0ts{IABJi<*rYw=e4=23YrZv4LY6cE4QGV zVxp%#cpzU3`E3+rWO)A6P-%7Z79Ye$zO7Gmpmia= zIh`}Ekj>ZG=S}!__&NMV`+W3BXZU9ARMCWxHT4UXiF`dA67O0*CCmy5{Fir#Flfi* zA5m!@wvk2R1)kykU;5$&t^xrt#bQP0vqME_UyS!YFu@2<;(R%8q@BX~;p%#Et{C?1 z$ePAjd4dW1L7LjD2o0ta;ftMUs-e=Q5TK;}PJm|2)UF#k7gXjlF}t-MMW?xxmS;ubchwocG71X($_x%vO; z@c+5=KSTbnuAu+bm5t-W|L*#~F8x<}&o}h^l}D58BSkh0j4+I}xQLoN>`^8{hPnjNz-mF8 zPOC`1s;IKKj4)=t$o^|&IuYIic*+7IIxGZ{d~u;3NveYUwuf}LBpyDula>arn-ivo zksGIRo@q#SbMh(wjnCJUm5zr5>z?VN#!1$qHukYKIz79w=&Zr?L@V=E( zy8RI0rGoueSDe}UuW!g;;Qr>o)I)B@d3_+awC$9*!>W&rt{3*K+wT-2n!Zy~E~w>p zbMo6w6W!u(NJA%tNr=>peRDU(rRdTudqXAPs^&_y#OM1M$m}Dj|2M4o!$AMtP~k>v zQ2AstE}$0aeAQ1;<@wR0Vx$@Nueb{(8A<~0`*4f_{zipE#}F#wcCzGQ$>ofMeq2n3 z{#U{P1D}fsTLm8i?q>x8Bx5A}l>`1lV4+>UNDCvQ^JG;3ctPB1Mq*NdR8A$!a6R~iX}^Z^zLy$1bn z^z9!l<;SkZ1N;VYZ8hW2C@4dvKtLn>&0nqw25y`H79U_XmJt^a5U}+l;AE-geSMH3 zv1CaWIk)qV0K~r(*icURgtb`2k=)+zi%oWXrDzgx@FA0C0-wJBWQX3PWpQ z2LgVxic|h2@A+@wox8~~X1jZGm^in@k;FW0)}LGc|vgr6FpSf)D%`)#RzsW}ol9+vK3k|XoP z`m*-HbCgRG^)I=WL$Ri8UtCV)cwBA&mVSU1{x9h$XMJJf-s*;5Zgn{<5u`^#cX`*0 z_;)the^2^H#hMZU5i5xncVt!seBSH7BlO3#)k)K2zfR#I+9rMHIBIaxy~SIr~TBr~|wLcK%j=KEUgu zpu5zNyCDSh#v)5ER-jPPW-(Gyyjvn}^-b&)@(Flu_V#C5X_1e~)@uEQMCdc}RwRK} zM3g?36|EzU1$A2?jbHTMU-EAzK-P+RCSzaR7ZNv6;0Qma0N&ZA`$>dAQH|(t=ed*J zHIalf2)9~|!KeZLmifC~Yd!>f2<5MMxrPc$#l6KUA|hhaqNYJ1rl52)sb`;3uE8Rs zUq$&ojMH$wbNO;H2-3Y7MHnAoG_aP4%`@>ICZF z>;>*)NW%7e+kw=TvB@e8C3~hPyYJIY7h)5OYm6mGa&p3wgF-M!Ygm&$?N0=LHUFC- z&M=??-?8?#>l3xiTw5>!Pj)siq|%@Owyo(WEJA-6u}6O}3Qq(&DSv`uFIXdgNjkm^ z_b)Baqwo(6`(j<%vilh)XXg?CpDGIY15_M8zSUe0#n&8eC2v!X#$%I-yxV`jt_^C| z3{LE&WzpOFi~UL%_dQH^PC9Z#DmO{Np+UrkiQGE%93=IZ>7W^}+abOL@?o$gN*0Jm zE}0ffzETO)zwwtsf(S=~5CU!`Z^By!0tycy`2EbWlNc#^`gmwvAhDQi{(|q#4!LT( z*(-j+EXMGYQ}lLHCBAuef3wG6E+Fp(h@DtXROmDX4xE+D*gXmIBBQG|`9xtSjvG6K z0=fK1o)ulU`*jI>8f34yDwD3HB)ZAnyd@fV*L!G`9(xNQ#klZ2VLwtKbF_v5%?TfT z0pY|Fy44{#C~E(w?ErHOJ~!NVcV|?`d2QFg;vst3`TB_GUh#2I6McWWDDTaNP-mw~ z4iJw~B@~-R0e5WcQB?pwgcA@y$1L&F62Cz@?6z3g<}K7|-sM$#Jl2r{B8dya-H`I< zzNcYQD%WD6Rcv&QtNOZ!^?&g(emF^x)gHQOsc~D4!Tmm);SzMHQtm)$uw9S~$Mm6p zyEcLec=sb-!#7NYox!|MGHdX^=D`0ybI`0@F03P{Ql08Mr!#Kd`nx$CxXtITAB_`C zOqrGeS6UpmWAWc4a`Y~>ILgQdgE!WnUv+&%4TY+21GwIHA>&=U4%d`6f8g3g8hvgc zm6K8$Q({BPkQ-4k+NRBsKaP{JBjfnfYW-360$p%884X5DyR~U zPR{MkS>WJ=+%^Bo601LQeTGs>t?)mbGw1#v&iRoFNI^wDM;A1$k}=OBHq$MH@yz(^ zzKzy4gByL@Zn;(7WyLpw)fBvkNtzb2*mOFP+@p5h(dHH=lwiQ5O3yy?{SDGbxc01* z)h_1K%}|$gT4m8>lg`!7h-d(hq=-l~D8Q)iJ9X45-UD@`aX^eB2B~0fn~|Q7=VhqT zK&15f=HUC2_^Nd%7T_%POH45vd~q_cWwlJ-)pcKb(2EBzOlFC-#vV3F26T`sKuP~oa!4Os*l z$a&~gTWD`KZeGCgGn@|p(bc+CUs_mS7>@KL`5}Np)gBwL((VB%SrPHw&*>>qHK+A^ zZa-l8KDQ|^h}v{*RHPufe?Rxf3kcC6dM@QwB6=Ml_CSb=>-R95Y0PD}EPcpnU3h!G z!9x?C@*TRd?gw3}6@T)jSJ{)0p+L_a!oQ6qeA&0=b$z5X9lM0PI+zVMLL!f5UJvH3 z4v9{agGQ?b;Hu;zZ4Qd20u_S+{G|3p*P|SDD^3+4x#&CI;-b~vm$79;=f_Jba@ zp1OQwbxn`w5OkvV{a;-1yam1JJJiU!*h!b_ZeYOaG|76LMa`%LOe!JQ6(t&@maI15 zU#aYV>naSZ;RO&{okrkADnbn7iXHP|r{mQ-Mm>J1>WlH+?viQq;xRsXlV!gbrxg6L z?ER~^UDajzT+&HVoc+3 z1Q$`5jbM?-WUgt?(F>mU%f!+-q_-9G4dpCCCzOkSvZT5V zW2=V}eT8kp)89KA5oRiqoxEx=?4jrO2I?34E_U)Qb307o%a+5R)2RkIUWzVEC1|9e zM-k4sS;@KL33a>(OI8(Y3^MVIIQVaz%1e3pMW=JeKMQ#LPCsH*EWr*c>E`1I;z+Fq z{kmusVP;+F@QS&|AYmvE68Nqr`i8yOKr1@%ipvjNE=5B)OW_@I(-G<#*$$WY_a`h&sV| zw<9@{eA0-6ynx3=0&l26;8_(9x5-#Wm38jw*CA**(6yq*rQi2e z@W-6*rNc7~Q-@VkYa%{2jG^brD++pElOp8JE(BBg(()s{iCWWoAgCzn5q*S2{}@?- zgi17frtq1+!*gfJCJB15r~YBCI=y4l9|L?&WGopkvA5;yDK&?6Yr?qa&FHV>?KL0$ zr8p>E=399o9^(GQhaA)7HHZG|BhnGpzsei{Ssecfy~}N8nf9>bmqf^8)F{wa9jDrr1m_J{9~B)`?Vyc>mc@k{V$r6a1K(d)au;?v2i;=) zkkFf44@BmopAPY+FM1JG?p3JT!G}z$&Nu*tG)~R-J^`k}yZ}M~S09o7~!2@zBOd(J_YN-4mpm&}0 z=AtdA*DuJWcd-X-Sa?ZCD^20WkWSv=;8TQ(zCdA^Ja)VnXW#agK?5jrAxBAw-%Z{$ zLJGDfj(LUJ;cVsRxtxRN|1;d?nV&g8OqFuFLDtK1skC1QgZ0QfYLR#6}?fYap5 z*AMZB7R!wh@$<2RBS~fp?}|uJ@PNW>g6VroAz8L=r&XQ5XQH@Qk*ce+E1K!!(&?A$ zrC%LP{ai>ZmKmIRftO>sgaElsY7C)rk^$`T%q`U+yH2?AkTcf=%cnKe1amswQ_`z6 zf4_<(`nTA_{}Y`5l%mImfbWKycH<-JzGSK%S$dA)SGQ2Kr87QjH~*lJs!o;YBqp8h zgeX0Br{%}7Hu*Aqg?%in?tor`z(MtG2j-7%CFMHYk)OK_f3j9d3rZUe6!dL$C9rI& zrp+VMiDghGa44{X3AuRsqp*f)g6Yzeizw}I&r0OP&C!1apEs?2h@Zy=7$LK~YP%D- zo}s2m*bYX^e^by)%^O)6Am%EH2a@G%s5)P8-#RJ+wtfSA{NL_iqfv^&(1h zwK#0_k)NE7cRk8{4T*Hbypwyl#GzNtrR8Gy3K}0am6=3b=2b}orkdQ}K?pwg6`!MP z;1Fbl`8HJ0meu#OED(tbV%I#^Qa z8{mgJXxfeYN0gS&)oxt$3}fj~mnZ^qPdoj;EH^VOxf0Wt6PAmWN2P|M8fN1Q-)!;sbnTdbchX4?1hCeTXa=edy(^B2rerhwin z17CTuem8Ht+l*9h-vrYJt$xTA)cqG-dqnnm8g0^W4sos6>%b@9txd%IX`4I6gZ* zGL_S&Sw#VDjbsCt#hMi9!2-qBa|<@$0G@47TE-N|yigOzyugHoSkLFb(41oP7}lFV zqJ$67$mGl1pj6oEyL-MOs_)SB8@_VPx_2p7BJxU3RtU18$gNwOAWCkL`7*G#(HACf zEOnVo@fv44+mATcX7Of1sE3}SOK$r|PZxhnt?Kq1gHqPg!YIWoWWkBxGh{Zi{`7~P z21sFq5j8ObV})YJk@P z3}~erS!=Xg79ShVHMQFNwf~7BK(`Z=*P3fht}d3l@G6G7GVwpA0IOL$)bb%9q_R_S!V;R*`64<5qv%_=C3`;Lu{zr*d6 z1nsW5tGXK3o7#hcn;)80ysbS$W7KMv)AD4HkqT<)NT5?ynW0ZCz96cK%d|4k`!oPZ zVoqg1AaL4Ys)siKjBg*#me6&Dp4FB8=y}t)(qsX_=RJeiT{@Z*Y%ubivzI#Nuoc0P0AnXt8R*c)S#K_~O8;e@Sv$14k{jVON-Gb)rIf ztSBrNMC;G#FD$G+1AyUsok8@95|8O%g=|vK#hoXP^So#`qQ>fjwL3xgGrQl9$8M|) z*#csS81`Pa#k@DiFR7hNXJ|H$o^|2hW;v|pN=2%=uETp53{Bl#dm&B6yS8-niwzG_ z45|Gc-md5O8>BSND9KEEg?3HWdD{$g6U%&VTPwWYTW@FliW>Qa?g$Jk;i~SkeSb3y zj(BMY>GC)!$&-`$)?HX7@Pk#elJj#OgCR%|_IL}ndKN~WXIo6F}Jw~VLQF-BLp-!}o z3#ZQD(wzvMp*Eb}`pYf0dUO=HofWQ}4MCQmIR>r7Zze_b(oQPWJ*UZhruZ5O4$~Lo;2` zsiMq^0b*HmKeH8zBxE(3iFc%)ykyt2v!n@99QfNd^;)eHsh%ex7XlHaul<(BXAjaW zY{r4Z`leJ-cSQ!eM*$^C-r!bCo#m6X@|jSpSl zM}XGeY>6>ZRq2JBfmqwM$D3ZM=(^cod@fd#ul6?G-rCK~iNc*Qa^|tHf z%$=p2YWG{`DJ^I?9V6M>dJw?)tAML+9j#!fFTn-fo= zF7*x+Jj`vWXcFf1GYR|hh@UNIXE~(O%4uz#j_)Gwd0kH|Oo@bSn{i<-_}M?-W?i-H zp8%)26jPXb?Uvf5Guy7Oo1=Hb{X%fvS#I0#-KZyaL+=P1g&#jN7UpC;mSf90`dQ ztM8n0WC$UT6n1l8a-21^ve%VBp?cr8hww{VpfOX&T5OK(QoVfm#BHbLwq}Xs%-o$u zr&1=nO{Kk^r>-C*8(#Dl)GCf;cuvQ3jLnp4hZNL^Lj0ROj{vf*5_35xB=s45c{l6i zZAf*Hcu`#`9I`HNK1mgCVQ7pJsGBqMMS>fl|8=CanAF9Ej*VYz( z$B`d%6d(QzA%s4-6e0BFsX1YQ@%6aYPl4pN}suroZGEU@P)1Tm7!yLS?GE3!N|WDL0tZkd_TcPBJ2K8d)OW{iuog(( z;alR`PH;tm%l@xf@(^2raq{jRe+eg9{4q!SxM2e`%g*HqI=7N<{rg2P$4;T-ebL zRx{;H;?x}`i9l=J&Ml{iD7t5sW%smAb57MFZ;J8^UY&FmA6K`TIVcFhqLZi+AZHuW z!eOhmT4BoV_T-Q}&=qSjm(!ML+E?u_2&%o*G#I^WuImj?T}okGj+kt zcHq}h*@SdJ@!)c8P~cF~%}BN9XmS$LFm8MBcDd4S?1E6o(|8bBjD*R`>u2ABwsj4P z6;x4owK~^ObxAj`6voE}+r|F8kFr-5-R*?vv%9*P0`y(J+id1PdlpozOE%=O(!!hC zYN^J@h{eXnXcV(lLKH|bt4sx|ze7mLn>Vk%!&QCV7F8+-wg(rKmrkEpX7N-CLXNi{lb5~7{-B!%_ibW2~sVT!pSJSKJPu=Sus(^rlhabsK(F^Q0E3?0|TYqRA8=v50 zZ_4727Q&ws`(>E;R@PfCME*vR%>lR*yI~618li?}!JQr!oG@Dn5<)!E&q+O(ZZF#Y4t<>mn=E*@w}3qIh$h+|nL zht9oBr>)d^v~218JYtB7VRjOu$A=lQ@nQ~IEIF*VO3*{U=GMbY7E)^(uG;I#QNhkM ztnsRgfXSDqT4!x1YBlag#@_w(+2?L#qxmFL8%6n}+1-dAIx^N)QTAxv((Ab@ov-?I znqV2OhcV<)qPmB5%=iXiQU(w{@#rG;RRr3@)xl>Foe9DBC&ZWr@t7dgog#es6)e%L zc~+qq!y-%tra2(D>;74mGutSkJAl-e{Tqp(Hzg!}n@6QdW{9am3gh^_uFhcG-6%^w zUQa}{O}a7|_oqqgeecs-ej{u>+)7*?ld%MUUX*k5TybP|d>*NYV<+1C-G04G-p26jDPX%$WP?ULd9pk+~*s@woS>!Q|11#4;Y9fp=YE$_k<=SU|dcY z#x1|vIY)>rMKfWqIX9MSw5Uk@43NM_4NBkjKo#{}{-N|U7Wrt zcr+v5Eb1$kv@2My*W^DCiR#)gv=6obk8bB{GuMsQ-*mvr z?!oZ##r8qL!mkbDa3%a{T%y^P+8@7Y@oPePewfK-vGLRlS;|=%IYzw+Jh)kYVMbM| zQx&FO#AL+JxWv^3#R_I_Qm1LKP@UC2EUKIXe|fjLqVa?Pzp3oz%ecAB%zbS^*Grt- z)I*lnZn9L^9!|;ug4p=N>@P1WPdw0caWwk068a22arv~(h?p=)P>J?|y~Y>UVmgCe zj-3pyAQD%Z9_PttG)5iOi<()As-MivCr5QLTKP7rv+(YD^6!711nYFH%AuS^BN3eR z2Qj!otW`aZD+{$$Tj;o2F0=-tNj6M~V-I!zpph%;GvfhLuKj6LveZU%r3N-j;>JR$ z<=+Ycotm}Hb)FEXsQ}1%(Ag%@0#{0b#sBZsR`(cw>>y2*C z~t`x z%?_t&;9PlVxBzbNH18I^XI+ge>roEwx1+CGt)!?F$k6qN9vMM1Ov}HXJ%a%X&?48boh6)!b?z>pR-?QZDs-j_4+^J*56MCZ zX|0wg0My0?RN(ryxfIn4-i|HKWbAyA@bE#5O3H0jqf?#u?yFbuDaJq;nr%{TWifMD z93m%Gp;&(h!iW)ng5X!;YM?qz$ujTcrf8FXROMVd%%!|nHV4V?DN zW5R}O+RRDP3FiZ5zL0MMl|N-?5z|hi?wdBAx;@e^k^51dUe%(d~K=SGDcCAi~-02aW_7pAZmT6``j&8GpMhm z7eDt^bbekJhx35>9D|rw)cPF=L34JpK&@XT1V_6WZ<9rK)nQ{1~S^hZWW zb-0Pz>K)i-KUa7bwl=%)AIyqeTDF9Rx8udgrrUm^CK!1*WT6})TyUuABam{aG`@S{yItCVzM;g% zTqV#rGHAe>*ucpa14R@ae@%lTe-*j8A(lCLwzfS~UvrdOWj_>VGe7b>-jeONW}J?G zZ=>7x;8=D^hg09am?1R}@@hOn9kY|LCKaUa1M>-)c|Q4$+;_D$yDc-K3k`z(twllxCUOsey|D`^CI-q`^vz0C3=v=Aj2N+wv~d zvb&t<6vf0EEEg5@>$vm!eRiecVNmBuvxX^JP z+TCG;;O82YI^9R-ly7CK3SS)4?c7v8S8%hMSp5y+B$U`MS;yGXIk8m7XX;v(@!rWY zoPcW{&gAofvgswkUEzfMHcLM@{mP6w0lhs*M>|ufL$X498Ymwid(6aqzS5Q|$bMqL-!^83nb9pKj0r`P)FtwNhgMbaHw}!Gt>7w3WUVW+*TZChxc)>sUgy_TH zgT2~3BnieYI8m5ks#;-Y%hdC(YdfSI7vC^zE!wTVoFTO`$TsG?sr7u3XGx+vnVE|d z^xIjf)2S;Z)-(_DkYv3G$+eB= z8mdqyt?n=PKa!S>4Fgb57dsw&lV*wTyk5P)MM^|W3WOB+D?q|mV46ii}nD6h`T$(*+j22IA zs0~c95)S7EarL2ZTlaSJ$IN*2JfR+DebCE4gcyf|zIut}`xQ-Ad}Nh<&Wclcgh7V= z7l4Z*K=}KmYUO}N?T8ht#o^9X0=Gk`#-RE?`m(^V{0H^&Z1KfY;}?3d8+W7U6|Efa zSgSVeKltS;9%?V5oCO&_y*%E>xu|iS&A7CUZpY2Xet*sZb-5=3#Fh&j3oJJ2Bu;d5 z(h!i)#n4$aVLI>{mpj~InDjbjcE=3z%QUNy=R!31nuMOI8?lGFaS>1MR$;a4oug)~ z1K`gKWX&2iZbgHgnGn9Tu1z>QS*ugqR_b;rh)F6W%p$uCM`lQoP7fDEC~vx19ZC?N z90x{tk}iP=w!)nqs8HYE-<(h`v+=Q0kdUBEBHY~d^vgO+R~sk200hlioH-@F+`k<; zFKKn@rWm|BSV^iCbaE=XC@#yIvzFaP9=%$53UR*Nk{}0$Nw?G$pvmJY{86XYkHX`{ z5e}E`nm4V9z_Gk!t{~>hDY-H`J-(*Aa!}n+%gkaF0W;I+op;Xmh9<#vN)Id_S8Qn| zF`pl%(YNOd1zZ0;W z*X@~Kuf>(=OIf<9%Vtvb|1E%V@hw6NlG^1I>1fp)JvRHaFAMMF|9&LcnRoE6W>(pRBCZF1%w~Yin~PE)5mve%KImSH&06Es!pZcOx4mX zCZl&NTkXG&W>Cd`gjl;($4SNT*XCE$4-`}V790HVm zCWUCY6{4myrYDY&EA6f`5ZH}pbvqk$r64z;?G^uVuji4J4-Tb|-dirU&gX!QaeBSW z62G~)#Pxp*f#FmBQP1FQ8Ld6~6?FmE^UA=W%8la#q|@eIZF{Z5f~jAAW}&yD-AnEV z+>G3pMV<+B-%6$@5fLKz`XmNIgl$rUQP8td+_D!vFAHS-9;xCqvshBUAvY!DAY%gV zskzyMY#uLM1}k4C2SyixhB;*(9}q%xUBQuemp4M)wrHb{Kx*&J21m78cSFQO(boC# za@DG@22(f0+>+Y(><<+YSKuT%g{XtMsR-Vsx-X7<#XngUX7X;jGxwC$^K}T1CJN~~ zQka`$a)n-a#SLtiD8zNmE4bT;{%m6+SHr-;$V4jX#!7BoElqYk%|p|qaxJO0o1^&I zm|(vp*>>P{yWr)8>rHaiav?NDrdmshycbme^QiU~Py}@+4bBenQjMTVS~DbqDrsW) zSDwC|F{DCSc&14PEk^#{9%F>M1*yzW6=43QJ?8Z*-did`om3U3ccYeKdIFxMc)=0vAocq# zTW2;}Cr+P{ylYpJatN*3jSF zAdGjw7u#FI(eX39xn#p^UX#z&;P1J^4|5GOd(Lv-!iy)d-(T z>)dFfr-zHZAS24>S(Wz-KRxCuU&4c9-tN}E-8C0s({W(alL#p&FMB=e&~iUg$6%Ml zG49?GnX1{4H(t{Dd|7*--?XqS(Q6k9~cEMZphKE4!Gj8jb*&~El1!F2ho>8_&pp| z(_Qs|4i8XhA+$n?;$L2Y6&A-rO7I2G+**dYjhc^V(oDP5mbq(Q&G$3R{oJ1}C~&a~ z=%92I#LH&TMAvR7n`cYVyA$pcXbR|cOodF{Zck^tC24lf8bHwm9|VRzE?F>j}Xu( z08;ZKo{kRGwy(Sk$rK3=@#roo8Z0M;@tj&~8@jvS(_lpesR?*4uRtrxyqh9sy$OMP z$^l{|575NT^bOxNcf(ICYXNGh#*wK^);2HS+cT_v3Spl&r3PjaOTkRs0BC8f4%T`p zG^>-o7ei;uet5Ngx8S!M|^kkM}0<9w)p}4q%f(2^3nba5hqhb66MAZ&w z3Fv4H=aLnr2cW3#tyyLr9n5Zu4mIeOVw=Z8+^_j^?gZ7jCG`TOWi zpwY?a0dt&XZJv57ou;=x_hoL4lDX;~PVNt48mi_DE3MiD(NEE&;?Z8(9t8K%O|%8y5xPF zaVOpv$ar5%Y#+HG(3F_o^ZL_R6tj5=NJD6Iv~i@pl-OX$dFH@{-E%}7)WGE>b>&|n zKb9t7Ab9UMmg-~1q66LYiGA~pnlMNOJmsR&8ybNt(Qi3pSBlk?T?P$_b>j4w_9zyF zGv1fV?s(*m>BYx@Pxrx2-%pXJML$$|p^vWGk$U@>scXGNn&RVulMA@YIvF8n+5eP_ zGL>8Fq}spwmsf_?KmL>76#EPXw=X5R?1k-0l{>-{o<%?4aLD{ zFq~(37!|p)$rZWC@rBBh6hWWw>yb6Q#h3V#1A5zkPE9XeLFmk^rPkMK(&sEDpl=l^ zAw}s1$%SE05BBR92G$t0fw6PW|C9Ax!8p>M=?S-hg3Xm=mJKyp1`99BD&+og|tNI6P$r{dm=A6CH-upWHy6)TAv3xiO ziqm|+X`Kw&js|1pY2}?yS5I?Uo^?F#QTZu-Lu*FMfB{>E01icD_Z)9^Rx)=5&nOJd zXe29xlL?5+O3Hz5VkpMqwbnZjH(c66X^Z#Jnd7m!%j}PYll|@y$>(DKs|65=5Y6bH z1;A6d^uzGEeqD@A3{~oq^Tq5%PZd`Pc;8WL1yc7q+eP@YAw33$Hyo7=Cu?3|lz*>M zp`WbBK*s2vOHAol8MauJEZ`eoVp+QJcvpS{K=H!7aanSz}`XE z;TD~8AnZT_Yn=CJVF-O@?!`TcwquV`J2VG)vaXJ3{b+C3asAbJ+`I5kBV~SZbg$?5lj@&f2w%yx^*d5JB`)#np>NIWM%2!sM+ zUN%uLkS5G|Rj5psJ6lm;tE}vC`VSZaHwQIr-H#eoW^Kz#8^RofP|tIvLC!<-Zm3@( z!pPMm3KI(&0i|SJ?Yde6=*sYK#?Uw>bq=#bNxoGHZo)YhRjy!_GT6ucs0wF7SchRI zi1+Gd3i7FzDRa5^_U-2zp{=zzi4na+{!Y*$B}aV5$Cxw^**2TgZA5e19;JH~b9Mg7 zbhWW;3xINRudpl{czXPf*)YG`S7f{quy(NF$?7^aQQjnUFJN5UzgWUeJpMrV zgYq|MQ|t|FT_D@vb)lBj@NipLXg`K1yORu%#8Y z9qT#>@z_HK8!gijD!I}PVq8}6S~j@eQ=DEJq0YH(e9WQ}Uwayhjfys;gUQKuM9zq_ zDD?e(NDB&hjIcLrG7;9>wY;`Cb-$ZS`8d34qk~{MkkKYbUSo;wUDZQxp>uDjMazts zNeia^#~!5ia+<4W4eM)h;UfmD_{*(OJU2v*_N!LomU`6Es$?EI7x*XzJt%;@;a(J` z?8Xo!9~4*Tb7dF9W^G56-oSI}ikRzF-Og3{F}SJ9bj>+IF<+P4xnVuY`X&=_86us= z&E)6lb7fDU8-Z`%p+Db7LnkH?UjNNv=8Q7SPl#2qKv>6kx-2Z}s~0yDhM%)4>q_Y2 zw_`V|qF3(p)60k>2eg@?1|aruEJ1Yc(|4pBsDDx_rL*H9CNkop)0YK4hT6u!FH0WKxy|8#O$C#@MRSDQ>O7>-WmF<2=-pxo`HTW z6Jy&`<0;6}Z~(6L;DQ&CxTL`1!?4u8`ZPYPw=oQ$Vdr&HLWj|kkwNR2*{xfH(V9LN zY^M`cLR(Igp)hPpGLlkAOj57O+JP z3S;jo?wM_HNW&%vPzwmGbObXAU$iZ>T~hM(43h_w#If8ZbIoDKv1vuDuXCriP-YR0 zm-HP|^Uc;Cx$jI})4=MP)W!9q&c(kNoGExhOqk2a%mb=h(GFL-h!3fw^wY-=*;Xay@PA)0cyR8Hg9vsQM z=zls!aBJhef>0#6GW@D?hV0SIc=+8ou9U~Y250M1(j?FM`QC3>!7pV0g(xZR?q@FD zm@zPj`qDPPNkM?-2j-xw@9Lu9w=y^@^wPBNSM}-@)~%U^`D`}=4(hmkHQln4a|_zv z-6HxjJF$a4DgY=1MHpLl9_*Gle&p0%VCAv7jumQb00)u0QJ8UUW7C*h!?0CD_^|*A zxc3$4Fa!VPY;Y$YVKzk|(TJhP^tNxwvaek9RW?z|vRhz4zqe3LPuyEE-MBi$O+cWM zYk8Zvg<=a~42uGyy8%N(A1fRsAk+W!>9Vh*lBS)_&4R*PbNPdh`N5AU(H$~OyAR?P z?xMk(YZI_S>hTy9AO35sI3+yQ2f0^;7-*+F#ohzvC%;@fxycOnNwMq$pc4JELbLWD zujrZer@1lPCGX4pQx~k19-AMUI>lD=?J;kp_V?F|d5z}$wO!yuX4!kxBq)jrWZcPqATVHMuogUebXYt)t}WB!oBR z=?`n-yIik=U6q%E%Yv+ad2^@IE`kALpxTCR&u|ry%xfddVbGtALBuv3e0lsdSSi{Q zhL2q~sdMP9D&3BuQ1+u;(Eb`mxFhNHL|q0J$5}gt!a(+%-I6%wnNq9udM_&rF@lih z?yz&?<$SVbv9@q>`kX#Jk=_ce6jT#$zjM)2K`}R$1uO@Xf_`+Us{Q~L8|%kq_uk|r zNwVf)6rKb{*L@1tgC&#mxP7?BxycaaeZqW#3?Q;&l4bs33CEkkx13acG&=EUqSm?t zX^C*|6jFTNg^!z{bav9nr?tytU+XK@xED>!q2Sq*ku6O=&4rc)X$ERNi7U|a0Tmoo zGLbx>#p_s;lG0L9#pN;>lyXr}NINmYz#4jmCbz z83B1X^>2BLXhO*>YPD+iC#81o8YB}UqeX9g!UnXl1@5ZC?{1D?+#bTU5G|*&5{E>Dv<3T4x_w37yTxRpWTXL0 zUFFyeE}jTGHk?j|U=-Uo&hZv_?u8|+^u(}8(fDKG#pi_m9Nl^OxsZ{$jzZm z_YS-rqtnU_)Iy7Wr70XvZA0Fm+YhG;KBzDpU>uiY)sd^r(&*AiX#UisuhU;W&(L5a zoV}g)j4T%!>`FP#-)3jY&LZ--E3!(L%e+^1+a+v1wFf$pFyhC4IRzM5vHG0?g4fG` z+Lm6I)3#4AO+5Sr}iEC4CIl_~b zG`P4WR~1myxl$iQ&j-c6`Rt+5w2HsakP(VY)=cLMj$lAGh9WL#FE-}+r-#c?fAQUD z05zs6N_9+75#F^Zb9$MK5k;VFeWukSt!zMcH4exkGX*#82rc`g<=Xp3#En8K6Ii8{P z7zyDH)~9GHVtqNfqW@_(Jar&y@f(W0n{!nz(VMJFs#m39cL+ z1HxT+nZn_ICeslsIYUrz8!P$tSibZu5w^>ngC`*ih-P2)=Tw=d6zW5qS^@n|*Q@I^ z?QO%HCxhy3REUH^i4O9*p=Q$TL}khOvYX@l&_KN+D*eI+K>)3{Ie)0%|IHhZ-j)!{ zH$z-i*du~60U12YJ;%Z#(4yXgU2QQFiARTN&)p+P(|>&U4cDajidb^#!Nj4w*UekC zo8=d$9C2NSzWP#x+^=Ac$)dG&hC#O~gV0T871^bmON`MbQGr5i!uTT1YL&B*9jLF& zC-FltBMd3~;Un;H$(%qzo?(WMPHb7gM5#G2sEggamwhuRN+fh%1$o2|gU_Ep0xqw% zG&}`l?|9xu9ga(d9LIrTLLlFRnFB$7=mpl^KqG4%eIPcl9cY^v` zY>eb{D8BFZ1G(i$z_|c!thQEBz`?#y2K2QeX2POtd#RppR87p??I|920Ac8-kjfwF z$=@47-WO+C@dWoKqSX!?14=X-=uk993|;%X-)v;i62P~%{>=vNPmVj(({bY})tSil z#f$LAD+P&mFYDjeX)(sT2nt9jQXCg>EZ4*ZHg$V{oIl?&i1Ev1JWw;9sYgN3Nlo() zN?YzCN_w0qEeFq|xIL2l%{>p>pQQw}<2{o?+~*?71b zf;N@Jq4jZ^_rnD#WZp}ENY^~j&O<(X3_zV>d zsYd>mJTOPnzv5VIzXWwJM>i{4iPO#+Kb}(gTFJ6O^`;&jn?#}{dCIqN zrfxWh62tRz9RvT_zL@T|i1Q**xsTtA!KydM!MLRTiM72wY6xH%QQHUL(^A}QaPZP$GuW^uGdi*4fXqhw{9Y%!ZNHmjjR5{bIdf~^( z*X)E5%N4zb%KOm|Ami-y@zFJ|N4^zN9nO9*CCV#|z_wG~Ak^I+n}rlbEeAfwyHg`T30O#dOxLr4*p7Md`JnQs%@Vkjj=3#CjcfSIa)$BWADX z>U%RqZ*@jbeM5caP7J3EE7e!Pj$-nRYNKV1<+HS5@$1uCfxH;YzAXvubYk`SllK>e zr8=tDv&wFB)5RvrD?@PcHJ!HTlyA`^P-mq&Ua;bIYROTh<0SFbQK7{bC5wr82FB-z zhGM=)b$AW@on*%2d4e8jR!pU|{0D4RjLpw=*CvaSj*rw>=GobR8SL*Ib91Wp4A4O(@7b`?A4y`^=s#^s{ATC z0Q2srqC748;mv5al4Eu<#u?Vm7L%g|W#n3QXH_i^bHA`^(JY4YV@Rl)LS*VZ*DvK+ zi^$(x9#|s8M0o7Z&}l;`<0=d=pR!)c`oW5RdEKjAB5fuWw`-hOx%r^4Cr5if`1XeD zuxrl7OWYhJm~$sg=9A1H+nFfN?$?;j=SF1ucuZ->t{L`2*=`haB$uLl#}g*EVsl${ zpD2|5WEFKev%gCo-CnMViFu=7+ID$TzOiOYROH;~Zc(p9&Tn0Y&%fogvnMstxPDbu zIlL-OY)D^q_|jY_U7V;7JrN{u%2mTNS?)%v{@S1QC0ZZPx_u)EH95{t$Hu@-oZH6t z$!D(AN9t8O^O+V0KHwJ0(|r~ZI*JNz+Uvs7`Ef+g@#%4gIf&QpO4WC68r{ASFlcWM zQBmnfmw|sPbl*D6$qPEC!!YJYQHVCR=0Pxwj9E1bT+l%cRk?JbTfF+m_IDS`S{=dM z^-z5(`uj{t2&#R7M)1ab^`m#j#5a163C!sG<&CalR8KeNyRW37Mmh|I6U7TNs&1nr zyL)|?i~A3Dwr+yQws$Mkt@h4Za&1Bf)Cd{Q_c&z7iDtpLg|$jF6y_?nla&(>N&y0w zlF58VA-0A)>1tQI=K}eNVjFu(QYTd?!|}kL+(zKS#*2ln0QaYj@O|>)S)Y}kolM^A zgtXEfO_$5a0haCTzptdHJ4K4X37(vM`+^o@le(F;Ul zfly!RPvy0s3B>UuBXKV*e&!W@ z5ndRH00Gb}%ZywpV~S9V=Za*5xZ^@Wpg0-^m(Eh%qY;K2k?D z8O_ROhTa-%gMJB#TwxGp81S2^w}_gGPuZg8PjroAA^>j#RrZmQBMg)Boz$o~;E~!p z=)9Z2vxXQxn@%~!-ZcQB5;~h^qcAz|d31*(U$Oc-&cYXj;gShFQOz?oQ1RoJ?aLwc zmS7TwxMC$*R`Yn5?HW*)tZ(I{s?zWrMP;kOX2w)(uug-qYF?(skBU~^?sirAc*8j5 zjzdV9$vH)0i*|mL$h#QuwNir;*5(QgN5^F4TG8ucTATZW2Qc7 zkiGHtT^9A1w#ot%$ETbN;v=)`&a2L9Z!MTvrZx%TT=Q{@h&0=NcN-qe1dA%^%*06H z9`LoI$5F`N?tsoC77ezdHvWVjGdkLFsy{(#uR&4psb;)U}!9f62XBm+iU=udS7{@3VQlSr9KA0j_ zYoj{%>*~$0sd<&Z?&(q8)iSx#Y9oulKdrGx;T4jW$6&uCW-dj$M#UEbhtB7UsUmGR z32kY528#pX2*!0JJja@(jcE_SuG|TVo5+xRZc)2nui^D9HrY}7`GO8BKzD^gtb=!qY*I|QN#D#En z(!uR0c>;oxRI?XZy@k=N?t&hQL^(W2)jBV@s?u^k9M)R!TbASaN$l9bSwA!RI_%Hq zMl(cS+I}r3U6T;}dir@3T9Segcm3QQNoVD(N-& zgIFQpQ5wLFy12hwc2HsQY;u_<82hpXA*<9~vn#Bfx^+aCa-FYXSXuWlao13Y$IH3r{+>@ zRGsb27{5N>hrI$6USKYsu1YMPI^O!1VY=*lkr4%(MWUzUO~6Zk`HTMUwF&e5_)2(X z`%Dc|OgQ|oXRYvPXK}!p@bmHIidmfvJnZJ9u!hzA9FgJYiYze(_Z!D~!dy~Li=F{H zVCsak_r+lo9c~2sO8lvnHC-7(% z(lI8GB*he*zFRWg(j7%lVB;j=E^uUzEjW_{f*>@|0x7dK44=7sPd$UAMq#`LIl9SI zZu_(>w^z{LjG>PH?lSi2#}{ul9S|b%h0;P2wQYR1<>{raD#>|>9d+xeACqu%W`mm~ zHLr(Pcte@d6o!jLMi^9iL?=>-_UF<80=ro=;S^Ur?Pho04Dxa<)Y)_EXu`VR-(w4*?KD@|yBK-aqTb=cb1lW{02+Q7fe_Bq3 z6Z3F(6C)Q(%k0;svp8oDqLCQp53zxtWCvLekkX=n#dJF|^YujKQ-{M!W>mgRdhE0E zIKb`eJg@5}VbPqbL6-S5GvWy8r(d2I@yqkb%U|HWMlPAJ6S_}n`6Pzdktw-(>+yKY zTgb*2V_iDe_3|iw$nmk)tB;;6`Q@bBcH5jhr$pXD^e+Pt64Xx&kxUb6+@a?iy$Sb4 zv7azWKkLI{V`HhOrZc z@@^G;`|L6HYK;XDMsgl2OExVd@%>_^HcSOKfEekXj8`c({fra=YOz76M<%{=0hmqJb<;DyD`*l!q#wB_n?S$?tI;J^B@~>8+E%LAI5}}pY`pofU$r|afGbweC_LLgq=|Czk8yZ5IN(mi<kR z$1?wX^A;7K;}b4R@sWMqDCMiWrC}$p+aH3KhjWLzStZr<-WEs&4$;$hF1fpL1>Xo!9-W|o99m7#ppxwbqn~?Y~{Zzt? z!GkAW%ajyuqA9;282tB32XM#GKD0b5=OETc0UMexWIS|n7@F$}N0KfxoyHu6d>Hbq zg<(B5hW6eZ6HQw$6U+6+UHty?BXiVS%gakGi98>%m z5=Dm-cZr67Z@J0Gi1K@D`M)g+UazDdT%`o0w7#O}YGh-c#NtNiG|tx%rmuEJ7ko%~ zMvLZ1i3HJMW&*coTg&dKL`+m8AWKSgiMZwGspD8q6_2_F7K1Uc{`!>xA70WwxRw4P zEgOwT1|amVSQ5DZj9RK0red6NngcmfIPIN!G#~!KB3|d(e#C;PA2mE`j;Sl;sgp05 z`K{}XaJ}3*Hlbux>|=a3(h?iIuXjs4#ERIz>74rSoskRq#}akq#4U--^Qc9)pO)(| zOMoLDd6}`b&s3xvzoot?tg-t0b=~-9T|o~KQuE9nY3kv5ST-A1z^l6xw5ydPlzK<_ zdw%z$QF!xp<%j=PsGdz5YbavMxMBK$4N_zqu~|eQG^&FO>E0Ru^#jyq_-5Mb)l3? zj^r=b?7@=@;RjdbX_lA#h>5w2K5r(_h5E8-$yEEa&Y7$Bs) z)A0k3w4uT~Upzg29EooKx3~TO|7IxZA78xxkIQuVACe?!VR*J^S!KMzy%2CYN1_Be z?O)%=k)qZ5UX!E8zrbTgH)oLV2r#$#W?psnUAx*?DnC&nuWC8X{IGf@f<}quucfTxBs(Y0D=@{Q8kyagadGC?2^|mRG#xA|k5}1qV z7b%sbp9?*Bh>VL+v-|Cj7cUY>O$tUY>$^qqjXU_NQause9f?o5Istbxd!Cnt24+siOQ& zk#C?qVtRkDCI5cJ_vHt7`j;hTl#+|=J6REC-zejMUe%28+?k;r^8Crp{xeL(vllJT zxM#H0=q!mtBK^XaQ0Z!-ygW>AA%8l?2VIPB3xT^g&$vR0J$@>~QR%)^l%;xlsBjx$ z{@H?lc;NL7VIRIf2+n-a$ox~@R$v`HdZxNiqt?~JLZykwhkv`X|9s?yHNr%w4M`MKb&{_FsUi9VF-&-~@d z3*1PAeSE8Hcek8;1ZH7j(b<2A<%|p{9>Q)*>zQ9o$&4b|n@iqYv-^}%M zMtvsKONd1~J~}$fA+bzn6)t`4HPl$$nmzYdXtAT6HHX^ezyIwYy5Mx`M$)pUbO6uAEE+z`aqdv&(@R~$rFp( z{nZ8sNqB5O3Fq4rYnrJ2@!^5^PRptv##%;Vr^XoZwtyI+BvmhgPuXZ`#fIhOU%q1j z@?_`qD5IYFDJ^|NIQ9ZSD_VI=2fCVtFS;RH=$N-Y!!=aCOZCUL_|JJ%R`1Dcle0E3 zde5!&&i;f)MBQC^R}82FjaDS!8g0&f3G;GuK_gRh(+$0ysl5O8vJf4dZ+sMaikzRQ z62m1qE)VAWqqFgUo$QHxE5X}QDA+g~#A2IQ+Pcl?ovXAw)(+0!V&bL+o&k5LEQzy< zX|b6EEQN7Mb6%R{%e{*zkj#ERrN~|N<75!+&%E`Y2etx`oYQ&vjq>1wis?d4)9Hl# zqwN9CxI-j7qv&hEXb-W)K|m!_#d%?-vDA(tFz1cxu;QdF2D!0`egh*}JxpWTNcM!_ zZ_M(AF;Yo#f(eN}i$)&*2|Cz$P}Z!FJ)@?q0zEf>e@nvc-um1e37i-RqPS)XFZ`aa zC$%GOWOP+OIXyjD!s)r~Qr9(-Az1Wx%c{%toro4wfr!irQJYF$Yl$Annr#vdrE=Gt zt|$hCwCcn!W{&kLgTo37{PQDYbKJ5wyr84e(PhZk{ON*Sq_5QUmU;fl0hVwr6&X!g zqQ9~l!=-~aNsSrwOQWjwWB6~UZ##$-xlSxqJhls=!2VX9Alul*%pssp9ai zr|h;ZAT@z_CcH2T#5wPW9Rs%F^Q2o(h*llmMeoHQTx;TXvEH7bpC6Q z=n?tfSWYV*QonDy#o%BB;G}k;j<3Q5b8j?0tu>xI{F9$hT%uI7!%NGr*rdk$;a67} z)-^SnTchVWIl;r?OF~IchNS+&FJ8PxsrsN}Y#e0Uz43Cc!Qiw1)gCz?X7hBqz(*wT zog#B%X_J@GF%|JRjgP-=3Lm@>d43OYn1RUiIp%N0Djcx%yX+jcc~otbH6%iO;_Wcx*Ti= zpJEY>CT&zT8?Kf$`(lB(T>msp_+_8Vk{>Y0_X_#a!;OD>rbb50cb7F+6J5r9FDa16^NmPSnDAXK%Ck|KcGJye2WK z@^&WS1fr+e(W01}Ox@iD%{sh=WO!U5x32!9KH87KCA87ioz-T=%{n$VmFQ&d#ae8f zhO(b0*0lfTNe^AnqhlzCcmdX5DG9hFm1a77{5yBYyF?eu>?mPP&s`T~OHK05wIuQi zmi>oa%T=+-=q$fN#Il9{!h1d-qiA?RmxRc?ghV5RfSa-&xLhGG+)R_O-~C!JL`Eh~ zW=w#MLk7sl@k9rgr-=S>q4O(!6P9Qt=OUX`FSX2LDNN3a1={n6%#^s@H7hI)rUm(( z`;l?kzQjH)TM{AQy4gBlaI;0mX5s6$#rg~5dXnksu*lwBUQ~fjR!@yEC}@CLcFr^@ z*J!){^aSFRQA(7)^8O3o6V_-Y;39ip7nk*tep@SJ*}vr1s5AhSa>xfnFe4laA~S|D zXqn1;K{FM(Li&2GfPb;CFWw{VwJv=??5NZ@6PfWT78+Tn0U*PE3{lIm^y^1gOIPR7 zPam%nB7xHcBFp$J zLZE#tm#v#cVX2nu9R!ntZ_s9bs?zZE%5geS&5mzEXa;~_PlS+gOjif*zs7xGN&Lu` zHSjN>Lq1%x%bf_~7NP-Z(}Q6#XGvDjKq zX70)1>N;k{Xx#?pAD1Qwuleu=`3c#l&atCnJg%x6{GpG3v9GQ2NC?|~M?SQT=>^?T zwW0lszkNZ0cy1?@xke8Y8GFX7li_bX7zqQHOLDNgR(!E6UAH=@m|v9q^oL^4Mby1BI_olYxT;>!s+>8LkztQVSlvCeovfE zLg%C2-5}&WpTkB=v#zVFrRS)-F9w4?DEQ%r)QOGP<8;T_5y13|2<|vm_nwjd*-R0* z31VUU$A?)E=nMSB7q8~n#xx&YdOZh~gzMzQWvC$hOMkUc3hzx7>DUK@qSAdU9SpAk zw7k}(0ma1wsdJPgeIInPbzCmn;4-g2GO#=QD4Pa!_{Loy1X2_kbUpCItl1W>5g{ z>(IgTnJk(u>gKRGgL0`cxS2sP1Io{7v)Fm3Rvl*rm?oqqm~iIuaL0!A;!^Dp-&XBMQ+I8L&VCxAyJ^8alRhRYy;xlf_kh(ppy7(AP%O$@V1G*CSa_PdL^Jc%x#(?`aWM!1mtE-{=2P2) zO(*ZGR*8iIbB+t_Eh1Wq7MN%3l|EOWsg27*jAtDalH=(0P!r_?rGORoYsUG}m|~@+ zasKWTKa*!F^_4e`FGsFU<@2Ba?CDYR>U2$b&!9Z44k^hbP50eJqBOxKlUBcKj?Qx@ zp-hTJ=ozJ{TTI5VZI4El=#4z8HR0kqFffZTz0yuGoz|dR8>|0r&UKl{mk`S-cBTIq zFm@QUe;O<>&sM7++ugtr>DRdt8|MVRY1>riGz3vctsq*c-XJ!=X}~7i81C$3bINnBVnf&ejn{A;R#wu< z#9ezk`E`Pf*D6x7Z^FHX@AxaAp};h_$jzg2N8YA1T;^6Xv}yf0~I zc6a0GV|w+aA$r>;npQ|ugIXDsM6XcVkA1^2J%4P>xCVW<I_t`@v@Z&SYJ4?EK{K5kW=|;W%9poYh(@qx6 zr=pio_5F=Dq2it;4jH0qr&J4xp&7l>R-I;3+4OockIgqgDyu0U&TfuQ%+x?D<0Web z)o=UQx$olRtooaT*289KVM+-wRAVfHpY?MX`b915vDZA!It_*k3oMnNuD2~0_1uua z5{iqKHHaWTD+eLz&K!`GpWeMr+jq99=U4+>se4Hz3X_*F zX{~TCZkwp;$X^!R52^-IG}WZN-C>RKPNF!O5fu%F$lu&CLiz}#M;*q0Hi%Sam;D$t z99T_|gt!9B8#irIr{&5N0a|Z`I<7*(AFDR3eq?xMuCD<( z)`#-?MXorP78tvt6g*#0Q|74cjawd?$sS}X+j4%be0omq;8EQ)reCOUaK{IA>9Z?> zUyrscj}0iLUK>hyh+HQ$f^H>i+Bs@fn$ira&fJeQG8?N@O4uCxE1`x=TJv|pzz^?G zf@c6SeM8sAVUhy5W4sHouSag#jsdxOpBaf*j^cy1;R93o?BR2Uc{~48JChyc7C(6K z;enX&N5$X7R9$AoLB;;iVNT)2+L{X@tqf+n=>hc(S_a%)q^F6?%SSSMEIF;1yQyO^ z-d!xE)eC{|!Vb9^KJhM7Tjnv=v5>x&OGRt9+_?c(n5WWwAD1mx`?TJNvCIsoa+3J6 z5R@>V&RAOM2DN{RL8fFD(e})Mv}C><9gNiw(;?NSicAqTPOkMxi2l|ba5XNS;1bGc z=kQ(evDWqXCE|(rDtpb(_&CZqmTY4Sft_|rm=69BUMP!v;3Wh% zt&5c#XCy7T8%cUt_)L?3^YLwnOpWV?14Xqln4_6j0*749$!X2;30Z8Zd|N5Xd|PQb zio}WEMi_mFKWt?L;U*~Cb6N>Pw z5J6e-H)`V&z^PnOiJ6(nuHBU+-x_<`1#37Di4`nmUF%VxZIK;gE?mu?I>rREQVJjNS{qk!G}&*&72TXI1-V z%rWz_IgD?m&r?ymW4MoxX)|*qV-v0f>=BZv=id`o9>9%kcy~sfAub;S%b!s@NdUh) z`cu4`uM$S#4aTw%-u>B#ohO7I;`E&9VQDeR&K3lEUQ<`@{Paq^5sa+oi?DZt&5~u&MFcx ztEZ#Dkoa!=uGa}mc&Y!aVmE$P;0DVHk8Q15eA$>tSM^q(`&%qiy2YfX^MYxO_) zj*dE?DQMZLpP}qtp=E9vCZ~AX>zyG_l9<9m(qp<4;xZc9l;HjdER&DB-PDg;*rM+W zf&nZTpHp#!nnDVKmYxGU21)8>t4`d3w}b3~%SDQ>2#zYBayqfh@YQHTZtjDVVZnu- zs{?eid#6{)k>OwD?qsWxs~CFKzqjWwE^avHL0ILzdZ*JPM#xR z9NAZHcJsHL$D`MlNzRl!3*$h_!h*ocrb4=s*_`z<c&$%_|f>Z zhr*sRLS<6EAx;r{e=-!LP5MU(HSRx$&O6fSH{CM=GYmL42Nah>kIrj&1lo|Xs~>B+ zuf20xt7SSVODBj^s&QK!G$fhV06sMj)qL9j&Z`+XP4=;<=+QcDj2lxiIWvek3LTSy zO^D#F@^$JyBJ8*7w6u0zVe~kaELp(n$V?aQZ&Nm>YfUb1t&ep}kz*uNK(A`oLgSKm zG`dOVWriOq;@MU&UF^Bs=8G^AJEpxR&WmkT@_M_1H(4T^qL}c=Gj$7Gn!g2ZVaHFg z0WKXkH^URcGnG~Pt&#_B3XEL}CH4vPEe3IZ{`?$w^lrcjcx2OS zdXrWE=JnQe=a#&*h;DzulqAImmN`@pHVVQvt}8R^vysYV`sy^TKNVVG55yLg?3M7m z4=cznFMZD%TFgk-T>Hsdk+?>am--#3cvORt1MbxBsW4{U%Vc1f^xT^7ol6MlBCJ50 zYu=^^{Xuih1wp=in>33h)9umf&^2AR8o}V(yN$4v(*7hvt^pa5J_zI2>y5gm9NL`q6@Lg=s7Sn6dIVN`*S>$hDC%qn9gmf_mr^hD5?UGV zY4Nz#CwbT7k=Hq9Uez+gZ@Hn6%`OAvRDZ3YML1@rS_#_gZkm$~Wz`x7GRKisH}3yz zblQXdSb!AB&uOo`hKL+N(6jWN`rnvd`tKrvmtDi{{GPc98u>F_n6Hap&79=C%dR6JY#_`Ffbv5Rxt`O4j0Jnl6TXb4)gJ{X~ zjl537y(q)k)F_y;Kn~*v=K__j7_xmCWf=Ox7ZoLyCa?pWG7R9-|BT=}2l&Nlvw?CV zBiVX^&4go$NplX9<*VH2Yc9QjAxHiR`&4E|)@T>QkO_}HS9dxPxcX?$jXzEArZG*z z0al~|&K+3VBX5TDXZJTu*rsxSEj-P1bWI3p>^@jPIbCCSt?7k$OD9I0u#7IEDsIZQ$Jo{WL8%(lCgUYhY5i`W4dyPJ z-p_=sKeQs^YW%m7MP%D7Xgf_=rU=l8IeLHzqp{t$d5J+_iRRq*(m(km$fzVcaIVXU zbX_|ofa#|JcD9k|0OKp%{Nit%$_WLytiYHhb54H*XKm3~*(A4G^zxe4`t~+hombey zOzH6YXa{rUU1%;MQy)s4dt;dF@S(Tyru4O~ehE(=P#}3^&5XpR(C|`y<*>7mw6-to zuKzWq9-qKq4Sn7C3rf}$dItfx>N@uzcgOyI?98V^8|5*wzHi0Ci>AHC$jyc5P6`L!4KeOTW=)Qswo+G0hdRg47Iq|&RlwaZtlpDw(7 z15stkZ-O-q$SsxedqG?E960+Q|5>G_&F$rjLslQF;J&^|4RPA}aAlea`(pAgsU{6lgT1Y2ydYKaPW~j8tZ~ zm1k7Um(|FZb{6g%<8?oeV7Z;0-}0M&K2`0KA|SDSbC*Ksmwdb48t8aS!|!}ll>41r>i%e75=KkZ};*dk`x*m%Ze%X)9ZH8#tp zw>Zd+ln?kmPsEF${3LP0w3K3=x7(M{khvu)X>HwdByehtv-HH-qHJiJeLIP^6&sKp zh{??>J<$}>k0&Z9lv+5CvRDK*89FHQ%-C1c{s+_p?dF}w%gBz`n)TH?CNm(ip91RD zLQMjyY;mA_vUbtU1}51a=k0l(kly)83d?T_sQ=usw;Cg%QF0|)%`kI2@6;hT>{z|> z5}L5?o3`#zyH;|ZCF@50M+H21f5M^ z5#;oM*S}?!%Pnk6l&Ia+RShao^*2gZ^UU$$Z{}jH(5ivPr){(g!}_7md*)*!F8zaY z++)W|=9@&`r{I#rCNOL=x=qTUU{{Wk?Gu*n?1W&g?` zIzdi~d0xN<{8nCpiqafFmGFpzRwBXRl$7z-nS|%d*GkthAM0BrpX~V9krFjFANtvS zZW?8lLQNh8?8E^DFWpjcKX><7jfYY;mLp_JZ=3cF{I6nbI`Wh1d>it8 zi|6zqErPKv2aOXFB`X3`N9hxHDgxzhi?R<(KW$yjE4OC+95HplD`+RgqPEy82BR6*!`16EW36pNlB2F{+2uadJhSSap zw=m5>+rAjp+AJ76S zf{f)#KaLYdf1rNhn)#U4z%U~5UbvcjQ!G~a#qh1%|3}$X21NBeZCyc12`On55NQOZ zQ;_bI76EDLW`Q-ppi`u!yBii1rAxZIV`&yx;yvr{|9;mmt{--}_s*H~%rno-nTt}a z?zFGfFR73eGdgYcH!`sw0jSs`CN5$#QG?ATUbbYaL4YSU9k!iTW_~e4m z&%P+Ja#!T`CYHN~bZhVAZ+y~V;XcxQm7n<~ZYdx4&Gv2Tyl^u(?i3|T6DT(Zb0yX} z6P!(79P1l$Z+|~^#E^yN<0-sOD?k~g1#c&C4l{QyWumgWc%00Tx4Inhx~8hb7@E_iLZueUziB0Y8z;M@3qjK<>aE^Oz&wS)24BIp10E% z?mlLC#QTW3xOmHZ)hoWX-G_DxCZ*l_+fc6*pveZE>R&eGY+@4%B)Yy8qxyamVx-88 zN_gt8$V@D7$UNK%r+=g2jmP?v@5DyxeYs8o$A8L&O^3x5tYt*So&A?x!L8MuHDyJa z9E-4v#yg%SA8})psSuuxJ*kPfie9?mnyaJ@c~5n`Y+6+3I(`>iUAgJJoz!~#6mcB& z_RuGit$hXgi?FzOVz;P&&2DJ+?onLuyh0O=nMajx8rI9r0YP&-gmMUugG*v-WU-w|^8^R+j1j+{yj1t)6~3pPm?ks@BGC|!$vi8i>}jiApnnvz*KMU@UA+h40~m!>fn zl@uQ&gzbrNaZcy5E_zKo;kvhp>+jV|NNT9r)!oa>$-9};kj;^_rD@vZ`ORyRG}SgI z{M%N-8|?n+dz+ofv8Bm~ullz@#W=QYx;WNA=epWKOSN;R(2rr0gYSOHSh&AWX(s?j zsx?Az@A(n`LikGA2VSliy#Z*2n$L}jld1!l-vGU31TaL^RM=sVZ1UAh~e{dwt(Wfp%k*lz4Z*XywK z*+TKY>2t23(AF(d(8hBUDb6A2_rrRkp9mU?3OL#{JbQtx-ZfA8F~7QiB`njTpAR#sWh*28ehr9e71kmP|wtMIMZKjY>-=2tAR_73XIF{HGU}WwM$!TQRPSz z9wB1d__We>*SFHOWajIoxOS!u&L*VqTuF1M_D+jW(Ycw$L3G)pOG5$G6rJacE;|V< zW>80Ij-!TWmueadNK5Chl#5dkw;2g36H%l6X`8$V)vNQ^^E$^;Yx&o1Rx>mjB1qF( z`!`Le+N*{5uEsE-9mKWOk6WxpEPW*G`*kx~mFx%Q2hPjC3|sn45Km7FDkOq;x`8&P z`}Qw8)g_cL?}#~`?6Vbjur(Ro8_b?&|4eH_IMVUZ+xcUGUcHK+5b|EU+bZ(Mgu5(N zx$9g><6B#s@QtrGJJjZXZ0JY$#j{pgVC&X?J;I@;%v*gjG$IFmE5swJS$6B*v(egn zP>*BTx>pJ^Z*BOQj4CP2+gUU4-tWu>ex=MUD%QRIO3piQ!{OE4vwXoJ>zuq{&iF`# zrOF|B($yjraNw&>tzY8)_e2&y(oE~Pm`sOrQmp4S<<-Mx#Es*Auh!^OejS?8vWP|a zhxWjvRGB16zi6)wBvzEEq^$#>xy^2s%Jdz`L2nPtyEaYi)J_sp&E7_KHx zNkJjmlkQit;-X-hmy&t&h&O)tJA6ZDWA$Nrn&gJm358n{^G1W3UzJoc6|}$i3g=)f zM7ccVuL{E0KNZk&L6JpRy~oajoyKy7ik*4SMDVhO*NxCBt6Bf6vo0Q+=v)mW0nPy! z-vSTl4wbcqwa_Qbl{n%(*6K_sejQ!_4o5ybEMjLakL#LeI-Q5V)S9&AaJ)Ml>iFpI zxHTxb3ekt}!lZh7D+~5-h&Rak9X2=8%pE3fKlt% zqf2IRJiL8}>GLQ^EX&@qT&yBp6}Lc(-bd?H;=e1vUxUz)&^zW_tG1uLM<90o!Fbxy zYwv)jNMJUvA z60ETO;l7TeuFD&=zpS(J#M&bb)vF`K5J^i~9{hv6s zAOWP26nR&U;)2E@P!x@Ko~|AqHK!0rZ3?E0DNISz<`n_?oZY!q)Y(}+?c*Wil6{^| z@3y2Xsk#1~?|gqJ5d7DcFJS=MU2b68Pr{`XjJ=FNdj)SZ(M)|MQ#2kecw6&b|My5d zGN{a4|2JZ(>c5Mk|H-|daR~+N%wAR~1X5{Xwro#&bdH0A>C3T|n$FIg5s2xEji9Kb z@U;AWQ>q@l6$?mEY&@>ezqEIaJq1?c$YME9VsyOgr>QUfFBaAV-5Vl(ui4BP9LI3+ z|CYJ^w?|I^4L-alA54RRPi&Sid;Hf&G5{hoMuY3V`}(@?1rD`0``EuYGNF|L zu=AvhlNSc8r6y75=3geQe>S_c68P;;tHNn+n3QcFLF2z1j3{^jP{4-wPUa=peRm$m zN&bs(NQ3JpDw*fBfKIr)yKP{~;9sxsN6@UqEr~uy^+rXcMdlx_IstI2B+1cTJft~o z+A{p|U#|L_6x^wiLzf}=twq+G(7(k>^!KXJO;4Y#as8RWNvGLpvcD|+p<^V>lMGz9 zuY9s|44b;@O^f2+hOj^169c#dmAhkx1#9{F#Zu;9`VHSHf)%P^v>(BTNhK)Xo&J}8 zLv(8acPlYreit|&79%qHFTdSF$Cl)Sk$Gccfi3QPxBtb?IH326J1uLH_c0`(z)Uo z_~{ru7XR>Dm;e#DE;BL31~)c!AZzR1KllbXz=!UHJY{zLl1kiH@-H6gjt)A^iPNeK zj#fyDjH`dSsw<#1GNY3`y5P6*xv32Q@Y?_m&=xuI^6QUTc>wSHng4Pgfj${rxAkDF ziiZypR5-VL=kLIV5EJGS;#RgjCcb2~?Ift}FVo9Etk`$Rz8>>iigVlTV=y9yLtb8< zW=GzvF;T=#hce;WK%INsOgx2T)i@(_SjtFWl#JW!o#cUfTN{$z+xvH@1Fu2U$-%O@ zIole7vlmZRV8(yt;t~ruiH(jbBU={|?#U;HU%w_mAcu%4%_2JQ5GBgT_vRiI!qr^V zE)S1=_T$Qkvs%tNhlCwsc2uZeP3(B-YnE!qHkQbQdbzI~+$d^~3qmBbH%2LQ-A#61 zGL+)AyB`pM32dbAsPXML6C~)j+6v|0txZ66^9-*^$BwwaY(>9mNthF~t-bsA)q{1G zq&K3^GG2v=oc>fuX-+MLUDrfJwK>pOPh~pLp$e~n({@$Yulmtt|zMs6Z z3eG~eHw*NP<+N`xRI1$BAsP!VtbQ|tzcY=is?*lNF51SDP1FAz?-Y?4$P^pq5EGPQ zya;b4G8W?ACbKH=e$A1~BPKZv< z&RO)zDxd40H;`=NVKm6d@kD!IKqDjSEcqK;LoeA;}$VPkZW$_PV zVB>GV#%#R`Bxlld+_>$Ae|fN14btV&!0FTHOUn0|Zt2pawqeI&+is_#;exKOtCe?R zJ=b~EOtt;V1 dX%nm5i3zLRL#PE}A*DU1H}qoVY8vV9&(L=O=buP zaKn5_CP2;ms=`hCPO2q)ZsN75Zo>qB3_PWUAF-7eh=<(Dtblvw@nhL-9100wp)CBPPXgO^97dS4{o6PoC zdtKrsPD6!DW^~F7U6tf4|1o~Ywu^QDHqHN#x_1$!@O@D{g3bFh*%)b&dw%{)dE-5J z>joX((kG=^vkFn4%M)9@s`YoeF+W2SH?LT?0PABz!{S1Lbebw4Hv!#De@Skbb}wOJ z=F_m-rloT73K4D0JyEZd>TV7jPsh>a5YEf2mI9YcZwj})nRO;wjR+8Ok6#@7e;Dj@ z%3cm>OOETgy^%?)xfG{5_0VYOm2B^k3z7T1fPiP{DChd}{xk<9D1s~t`gcN9^F1|} z`9zUQi+Dr3+A-5eHdIJVeXB2jjyom6>qxIARyR6SQf*WC`8)+;Rg5YGYk6H(F+;;A z?J8uyt1WBI^y#8g2o`@HpLoB=?UPZ|(%u!{z@D>g?-Szyd$2Prz|K@uRb3L2o#S?{ zstEqOZ0HN-WeH03^l(hPyI#sWnNfaUymgkVg$Jt|I+L4Jw%Z}DQ0Zr$=WB_0Gtv^+ zOLld6=HG5B{PobXcGgFZ_A5A~RJcN{QMNl;CYAp>ht@;%IKyyowzTlV0obeJ2N~EY zAc;?lv1EP&WpKPJY4}wpl?I~RqwKtJ&T?9(0!2JBV)dT?F{u|MyHmVLT1chrM5I#4M)LZbS)T_Ne)7|rPJQND=6_n{ z=o2AR{O>iR{!0>sSesWhWHANlU~4!-{7>48s~Xvv`%+#bpE=O_HBLP~udC`So@Q3P zLhVq;;8v#lC`H$LghtjKgwm&#!elza6+w*QiAwSbbBGE`@#}-iA)0z4cT}{}TE3?5 zXOquH^H`3lm1TDg1TlXabvP1sE<$~>qYns(L3^W@`~5L~ND$G_Olu4Xc7@z$+J_=( zkoObf;Yp5xc$KO1@!oB-(HH_x7V3S>&DXz6al9MTeNTwWA$)0KNw1E=qoDo6H})R( z-&d}beTGLO^y9tk)b*CnM<#8q%`6nqTi1U|dK0P>FuD(4zKQJ1{Gx(Sjz0E!{~_US zvi!7glJ#6N%bUZ;f5c%zwdPky<&9){loTf-0iyDd3vE_Da&A9PLBF`{3u9H(1#MGb z`RUNW#XDjFXT+EDmGaR{;|q2FoUliYvK>&fo$!o|Dt}#LU3FG*e^bryIJubG≠R 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�lO8DW0NKi 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

    w1t5P$prYXI;< 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 |Rf``VJ7$8XzAxaqUHnm9uTJMJ88 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�?ekIh)d zc&$d;iPRv{qV zGkTQzjLHGnv<+77+lCyn8i1O-!nC2P=pb3fg?&-slTYu@vl6{_@-wdL= z{yYW-T0MnOufh5PC=!+8)*S0rAxYD$e|GtMoDR;8JVy?>D^`?S(D$_k58VxwxWcM=6lHKHK9_l_8x~gxLxW`v|nRb%B8HhZuvlX-~<#j9lTNUS5_sfhRWUrghc zL5MaD?sy>&muO9g@t%&XCODAkFe#r{Jqagau}o<+)Aurq=d!Vk=dv=4DPE&KF0=p$ z6qGWgU##1g3h@TN~p089_jk}i0=R}^&&ikYWBYp0C5fF!T z_6|QDrd#TfWhfw=xn6(}tuAw$Nv#B(A|P@`HQ##fU7bnx6R$X6`@kJ>M$aIzV-^fs~+?PZbCx0L&0s2S&5T9iOUXUvLTOetf7!_76S)R7_h4 zskhEi4FcdNbTZyME>*DMv-QX*q#c35)53NvyL0tPUpXxEAH1 ztFe65*J;%AH8*e3o2JvqtTqAB?|8Z3w&^ybY6?g!lslg>aJa718mYemokS$lT1Gw9l8Ja=%atu22(Ti z=1Ric#=Q$yD)%nsO=ERk*tk(|TMF&jjePUxzNp+Y#Jot>N>mSNTkP z3zUEcsScMT`{U|)=5}q*s^6@?t}i%Vl)K>^RqXZmm3K5MwCYhS*EOjK=5%?=?|2qH zCV4`wK#I3f29*xw0${rfyJ<6nRVv6Ot6ElZEH+bDtJ+9X)dTalc%}q**Drm2*K<1b zPjZ>L81hTgN0chQ?et{#g>`(O5X$o|1HAj?jlIf>%LAsK)dZVBIU{u#H>5(aNmsl= zlEl27?8e-?Y)4UG+@zqoTB|`KUW-S+2$WXr=Yv$u8S2;??=&zETzX;)PIo0$-R_?4e>1%H>=AtP>bYVgWAn#K#XQzEqtx7)B ztfEvHuJj#PU%(}e?8}^<1ui?LmoeX3xX#lC?C|-WQgCLK%#xi8wocya*J4d3 zI;2kw9HTLnd!?j!9(*ZgU?#GPTTh<^K{0d$>c8xWV31iHyaCyJ-FcAXt%|L1wwQA90PBpoQK-OnG1*&+GHmyv$!lE}fXRo*ffL6! zH5~UuFFvVmA6~-8ZaBgO&);p`L|ynHAM?~~U(%dy^eMNwA|GwcJYFL|*Guu(&6+-0 z7&j>Be<4DEa<|TWE$P}gZ!o)RwVEXT05(?`>j8P*av;;xu67jOO|s=>KblnwRM=t zF0JG80)%&F2rF>kcc*-Y+I_T3Ddzc|tM+^aJ`Pz-_+{q9Fh^3escX=B$*F0_!j^&7 zwc^YXobzZzY^=-ubpKK9=*iH!2Z{=&?E(6_9ogb2@-kB` zNigog+Ss}?*F0*|_uh#_M1OINI3|HZ!0E%JrYk08=I(7gr~GS!LkI3urnW9@9PnK_g*Y$gG=o7dYkPo+>=q zyse=j9dY5_K5dT^kN-19h-}n``K&}tfo4YX$X*zY(cFnF znR6PDI<78~uv19cJ@0!;ih+s7k*h-S2S-zppG)0KFLv%Iv6j~#(PuvZ?fvpfgf-?F zB><;ULTD26dt&P}km6B}2J{P{M+>kQ$y#EROO&zKo zsv?lQW1jY$jTL2y{N@=SxTdtms`dq`NV9d3_qV~XF|AQ0jR-){3&Bh+@7Nj18$>8wj#aBHr0tfGJ3ibOZ_#UfM&a? zqv|yMWOVzLk84VNsTo;gAyBp8b zl*R#VtK@rU2}2zVcM|Pa?U>uvn;z^G6qb4CbvKU9U2-c>SdEv%ggvB{Oz3XX#BI3} ztZS8U@d+#@ebbIJ(1YJiTD(`E%&sR7W5>VKsHkCKbZ+9yyF>4=njTcxXfo?7I+C+< zm+3{(;Zk(DzTTs)-;5&E^)F&;vL@t`_E*O2-)$zZS$|sx#8M7HPV|7C=n#5t|&YBvCdnk;R8o!;)f42Am^B22k zmzg_<=5EVrtAX9st4C~>F%R^oHOHVIDbASZ^>k)h>N4x`r`y>EDh#_rv) zqO?@>i+qdP#}F#yMkX(^!yD*M_I2p(n7W8ziVXTjdr`i>zDT#pk*a>-EKi?!&mQSr1>iUDbd+g5&9eL%w@Z zr0lZZ^8rQu*|}~gaG;mZXxZin$9{#$h-EqagIMR0jUZJ;Vh{$+0>c$L*}X{AJu)Vz zU_zIgiG?tqj{WQ{O81dT+jVdlCoe3 zH(HlurH}T1A=Y_eq-@XrC~s5&8`E7OXe9wjJrMll) zeM#{Cbb5U`S6F*q%~#Rg+V@BdKYH(KeXcc>h%z*@m?sIT@DIYdRAvN+9P`GQ!6aax zG$6cP5uC)L8SU~tnky1i*H>!VaVqk{gGBTGYJuWwt&hV5ZEY(zOz%HPQV=LBG5`2GAY&qJ0qsX&VA$0uD3%-qryf`Tl##Dy+f z`c`o<3>?ebR_X^+*H}OZ?dymBM)a5ywaT{k?&DXX>x@aD_M*g3t(75{FX}F?&HV}Q zzr|y--2jU>jd13$um$`1)IA7p@#7%I)R5VS`S|DwB$VeQ8~5>-Bqi;S0h9y-VIDA# zj_L85hQn2GVBO6%WEHLEfZUXzprmioMQwSsUzAx+`+PSY`Dq|C7|kebp>QDK|?w9VG%VK1lj-OuV8PfJ(AA(FZ9Wr^Blh?D8{ z=~7;;`)6bxaX&aAW2rXRN>7J$>HJ)80sGe!ACVnHBC2HN=5vQv2X-6QZ*VyI55-{% z@$=!_GqR@A6TbuWQ6DyTO@L>e6-pnJy=y!n;m4>K(U=$)DNPPi%?BEf4r^sz4BX>) z;ho8+&4G6FP!~8}Qm`~E@^4=G+O~kDYPd~gT(;ms^{cV<88v9LSh2ao@9?XDyavj_>z8#|nP;f;a z9MP9{GS|-Q zyuZcHvcsmcB(x|p)!GqK3CeUU7w@-qUf;WE?edGB3D7dy>@3GXS+W`C$$Rf#{@pCa zYeCIrgrN@4y__;B@A{#8jN)8-NCGLw;^G-Z$)={9CShoMu4+x!TD|e2XI&y@04rgzfiM@Fq67$P1+Y4GC}g8 z-2zC$cqerwApSQT3L+zp3a z%T~i?OysrT+mZQ8GRb*`k|XF@gR{^PY@s3#69rQymn>`JLez8Jr?P+dE|oxBw5KpJ4>{3Ohc>A)uqt+_}una&pV|hHEkT;(`-zRE?77U&-God?|0H%JIzRl za3zx*UE%TX3Bqh?R@dt*m5+pE3UL7+HR>yg@dxSUwyJRuf9N?V#39+dU85-QoA4gF z;8b~MYQttybW2!4Kovu-tbbO;{QiINsz19SFD*aLxqZQn)eckOc@2R7Y| zNls!>GiL#Y8r{weY1%{Y4)D_>bb?%p$5m_kuAOBzmoRiEwe8FR?od)0_Qo*oVEOOu z^Xgk9{+G=@lf~4ht$k>%!gplm4Ck!A(LYtQt{*>+#N|)DNUPK7i3=}^VZ4pMsU(;?DV5IXOw9Kc zO=;PAt*BZzBbHFYKX2R{JbQWOhGenwoD}1|WXye}qnMJCM$}&APymx`o>OW=;Vf(D zA=KkpV28Yk%)f~`B8`5@G~`KCgK>6SJtfj~zyj(0%^IJ`6w~4khmnA0@zeM@rR@ua z$5>s(0Cl5_+$Vq2=xBBMydnPU%&kqP4T73AfxeXC6e$-ulgu#iv&jsA@vI@L$YLc| z;kqM3I2zvsp&{VgMfscVLz`QqFLz>^8S*O)O&28eC7-S5*)v3Uk6NeQLQA4mQ!Y3! z>$1VBN(^BMu|>VIT+P$Wu9}&|a({Rf{^SaEL$v>_l{jtTd1Sz0+=0koSkcMtNg0c! z6f1Sfi?g+qBvGtMQ_Z8VQRyqLLn~o6I73|@o(CEvN4jF%JQFgXEG@N>7_7e)OMij2 zq7ixrz6(h9fFtte7z-lJB)>3j|kQuKEiC?>qA>F608k^T%1|k%`dYSC70;lWZFL%a?}hO7`m* zGtF=rVkwy2nYS@A6IQNZ?a?bd^}gfhMD5?o8_E++K4aLUb>f;;s?h38UpYi)>G!J_ zA+fJ8lhpQy370#Erm5)E&y!bt*QPs9&IH&@A5iqK(4=+~!E&OleH_2LN}oNY6_(Qc zf+@A4u}vut8Kavg8%b|<_;N_QlSf_dHPBKp9G8{kzecvCzf1BOY6aC}8bb6Y$n&qD zgGD$u;4V?w@5L#M%1{=gs~!T?U9eZH7LEnLnJo*Iy_Bg2b%s&yF0j zeo!59_)aWsIXu7VY3{MX*@=i4!J0-PBfYh5hdltRkGcCv)ktSZ`9@M?dz`AeX4c|? zsPDhH5wL>XA*$K8fuy4U5Phmdu1{E(^-?htzs8EOHmqmlN$XHQz za_^ePA{kU1<|wd<*5qPVGvNazBL9qf{)DaEhz{i zLYGjC)?oM@xiSE%>1f(fq{J5hNi%01;Bo56H;oM8K?P_J{>HBVOo;jsgW4msfs3{| zw&}dG(RetoG;ZDZ`<^#fSlM3f%=e}!5e>OO*Z-PqKjz90iaehcZv$Lr-vJ&G^(Qm) zzE{1>0ylK$y=Z?Q(=IAt+Pdh&v?2ol;`V@QsR!rBQ?M5h$37kso60nR!P6N*gv=Eo zU-ekG5)|HaOva?eXr`uR(dr=G@Tvu36A2Kj%5fOAg&{FWaa$TQc1KUsKiLcC&ZB(0s}-pQe%n zo&R0p<+BN5hnmbsM*xWuhO8&F$p3t#zhohXv2Mp54d-d9l)P>dLr`uxc$AzgSw}_u zCmoV5S%6B=q*c?03gDQ(qGOPi{T0^yXoS{7v?8kWBYo@+$UsAtte*t_8ymC(# zCkY9XHDILi<7oZJv;U7F{NY25bAU@7w87vQYlM&#xYuv}D+K1x!V#O`N#!|tFz`rq zcj^8Y89ac3`(OVJGXos+qs*{C89-VDPX!nE-G7>jf7#K0<3#`eS>~isS!_OMqNJC_ zaW$U)qOjGW`xC%KV?o#H)@ypWma-NHbR)(~1`#9xm6-&yZ5h( z02rYFT7g6-Ji8kk19Qkeo#o#zFyD9@MO9T3z3a7H)8X~~RiF^hL(_!Mq!q`UBoHT# zB|N~`;2D(Yak_uD78deE=1(pF{Wv4-Buh1IXFLu-;1j`GymmH-S$8pOiOA*;U9@fW zcwbyUt$w;2?gzGSyuOHoVhRmhx}E601X%CtYN|xSQxLS%f>%9`@&yQz>JfE)ruHEf z1(B`GWnTPKUqSgK_P#uTKYn$q@WxDwd}#7bKOni->%Nswf_}AWb;AxM@cMe3o8I2b z28tIw?G!$i^5)LL8}0@KLzVWmzUfg=fE3jNr~<7RqoYbl|7;L^-4O)-@aoe*f@CB} zYl+eGpWXHQ{_{P+iQ`x!82Tco&f#?~@FBc;+;;t=b}u!FUDT2S`pi5pz`s#4Q?9)> zed6C_ZA$Aq`GYk)`l07+?HPjhoQtLQ)FHIy<_=O2lyPj%u1vCziIB8hV2Skb z8kvIcM3_g{?s&!wMk;a+>Buk{K5={bGb1;^icn!Nh%#E$8JK6~U`P7Fvi$u=S`J@* zpM3UMozWS=i|HU{Q^E%4x}uUZC`CD2H*?KjT#J>e$Md&NXo5*>y(<7-z9)w3NRW>U zy36*R{)G5#M5NaSH8w9Q3f747lr>DCoHmm!1?i_*M7*w*fTDb%E+bQz;INL{S*uHH zL!0-}?n&K=kg@Idj7arK4p}*mI1-%aWjyDH>H4cO-^McoQI5-CI>A z1b^cAUU$U)`tmG1rOgFxnWV9i}#c1mU;t7Y*RCOYNW=? zclfJS?-kZA8ag_4Lk}l@$sQ2JA} znhqNwwiINB`O4;L%f;>|b8ZbEp*3~%nY|2BB*;hrSSVlAsGx~*@n#+x_5$b@Z!xjf z`iD|M){bT$Ryid(n?oMQqgtS}ylR}w@Pur#qhH^55Q`ud#;Aj{)+h??C0nAYS(gqY zH7;s?jL8(_sBti-s_w)gT_*TKpvo#7f&_6hU;q1T|9OoOh%lg==>fjxWuPH&lX-=)NlD`)wb!1 zP{=TK#>Gt=%oJdIsx*WInT%s$&R-a91hsSs^dM}u{6-WP`^ z2?;JX3ZoUSf7zXn&*m2ZzyLs-?vi^)p-k$+K9_`AV!?I;=9q|+8k#ZJd#6WN1;$k` zno>!V4?0B@Oqs$gcGA(ZxBz_-7iZ}4Dv+<>PU^F@leYs0x^)w)m1`Sn@cm(Gt?V5N z%ED7v^h2dD9y8C4ADjV<*c~mZ2iNU86&%oj>CN&Nqu+FfuGmWeVz_vycJh2nNiU?6q$*A)J_9)xcZJ%k6#2K>$&o4X2t1_MG&olpB=%H--3N4>Ss z`sagoHp^?>1a4XmII%FpN@4g>$?7+Tf?hUd^iHa_VC;oyfFk@wIkuZQXgwD9&#t+@ zJ8jRPh#=(l`(n&iy*h>U^4+b^gsR`?6>0Zh{KVbBh(wr8(<=NPJfQYX zm5%(Q+3a}y2nmDqdB3>$pTFvO@fsNN2|RRzfGytY-{?xnccV;~~UGAKGSB z^7lDD5OcYN;4m24(H_sDakrp;euNTuJCN&G!pJb4jW}}LL$XyCC#wP6aF}w@SMRvC zvpb{h+I!RJHIWS<9%g?TFr$P7khv_IW&s zG1I9Ql>W)+JsG${QCujxh}KknlHF{e%F6!WU&qfEMtF;!)Vm!uapLztIghkyC%z8%dlLD&cYFqL z^vnaDc|ihwjz63WHC>q_ql<`TTFihP2d}+A{!bbEACjONEG*_nAqZe{*&=b=`Jb zXu^C!^(sH|mcLPZFsPgP;tn#yn^CTc(n_OHp;m3t(wMvXDTcD%qlM*ipgwxiN!~{I zv3na!tUy0JKV7~?Y>p8b7CWy=8!Fg$ZOSzV{R2fyt`eLZ()-E zWrY5D@ht4|Gk++#dgcVXgg?|easB^jcrpTGv?fuV)$H}h5YwhKF8Q1#2UHv-+L9YF?E?p|!_|4p zIvy!bA3f6|@5=@3F*_oTSqyT1^qOzW2OG8*I|umq`G$Il&Jq-<@P4DPn~@~%)w65! z?fl0xfW(p^OAfP<&b$(4sqWiDwFT^Uh3hK|@M5Q#;h681s=Cf1zP7cJPo7UI_J^n8 z#hmOql{%%p>JiP9%@`DHlQX=FrrYlAE)fy)F+ZM8fLU~>23GztVAtar=t=P<|F2yi z2z?{J?MPKeMx6XdOym0v<=MtgEgG{kme6Gm?d6~}*(&#}`JIXZF*Nrqe%f=Po0J>L zF<3HdTYiz)Y;{_4X*bH!bf19DS}fyr6IP5%QReA%!_5+N&7~!L_k6b^cFSjYVUW!w zkdD;w{=%%s5nK*ys=N_6zL{f)!;b8A`PQkB(LTsv%7#ttRMs?1!`_Qe`vQL3zQ`0!Ca_SE@{}nM6s~I|?nkRz4=W(yW>>D4@NSzHoMs8h zyDsUvU5`xR9_6vM1NKtvmRef;+2}k^UBgaoHt478j&Kn3hem(~IetvrxZdV3?*HJV zhVsW+`AqH$s1XE>G#pNgB)eyuXYydyj9zW$h=jr+d+jN%?%P}*54zr)CAt_IQz{kW zsqnrNq1^-4kcn`IeR^XBY_=|mStj^cwAdsqpXyPJY1=`!Mu29Lt|@=-*GHFDHFq!4 znkoBwKM9OsRIWRm>^=5rGf4NF7Dv)o3NLG|m~$K3se?5MW0XfeP&&N*XJHTJC%>vpK2f`!PDhPnccpcoAtJ7e&O2XeCpkjlil6e1nXr> zOUp@Jb$@87Nq9b&NGY<^oKu~^@#zq09_jAj#mR2w!}Inwn}>jc(AtdBmBK-eErmc= z{Rx5N42Z7v$Nk?@fP7@_6sgOv19v|MB})~yx_gjm|Gj&VqX#B6vX`PeO1>_ z^(B3i#onMogtDH<^&sP6%H5TM{pk4o>B<71lZpL?k7d>qw=ncIH`g(Ux%#=%(|Sc# zjgtiK9Vsz2fh{;N9ex}#ZPY_ysay?z?xuakSa0ePMFB0x%H_g z9M!ZmrqW+FDxEwX66%_TP;u#;S8! z2wFSIe&U?{_o?(vmP?Jq!<5Wty=ho`uq(!Q_|-4pM+^;62lfPBkolDc?(cko|82|v z@Z!~kzVa%(I%Y{_vQptT8!HhRJIA+%=?&kJKY+`x)MzDC$U^AUD@2Y(!yhVD?4$unp(Prn zm22?x^SpeGdaorW2A`*}s`X_e=Y(|G?q;_|)by`NEdwX$>)T0RL2(6ypEv!)=C4(W3eiMWY|0`m>?FTaB97!SPp3rPctY zPIPNO8qR33$@Gj1lfc6yA>Pzc5%<$qo}ecc+b_DO=g1^1YtsKT`r-YbdLlpF>wo|7 z$ozKFXHaF#n~;&z!$#F6rgWD0cWLYfwUSU;iLe=q60x}c^c;;h-{KymGk?)pO9g$# z$0XU@qC$f_zk`pVBsIP&vCrDjTjkcvWV@KzDZKBjuFuSJ_vRPp{U47*$ovI8i`%?% zWdzmWqk?X&VcPz{CfmW-wX%STWJpb}HQLcV)z71mX>Sd#9WJm(_4PEdg_okS4uMD0 z!o%D`WhA8{&jr2L&}x~cJZ)|^DA`t7z<#yyK=Sa%=2AmD#x(#Rd%qy}iKcBE9OJ>nwSo6LSQ zjz6Hy_>RU06(6<12?Gwdk)!9~taMjmALc#$b{es-|105FtxznHYcX8REgd7HIxE~(DF4|C6Y1#r$JrE#)cr5b zlrgW81y~9}?>R@cjohq%X_kR7jn}tNh4%;_Q&b)A-c;#oUIZoX>?AJIh}~9IIg}P( zTaTOEqdpgw8<7%S5)zsS4}3#?=vLzIecvQut=VLUus`!up==O)Le*#>nNyiS2VW%IN9oxsH2=MV>jTll zkJR1=Rg5{)^RcgNv0I_;O8h@ za8F}@<%xmF4hM1SbG0^8zM4Ad^8FSo@;)>_B8{HP{c;g&Y+i_>19mE+HaL*d;bD0= zpnE*&ba^mAJ4_hmsNq_z+B9(L&*MYub0GW1tyR3xe%!r@fl78fOc)0Z@-5RrTs+C@ zK`T^?mTYvSa#D-&OCnZE_pR4KBBKt<{?HGR)N*{hO1wIw`Z?dz+8cju*&5w-f zN(YU9zuj9YICYubmiygcj-E)YPQDNd&eRwu<}Nge4aTPxAYWU ziye##|yK1n=LY&jB--F(dF;KfdmBfzTQ+`DI(Ar>OfPu6t z+F3oLnG$92myT_P->pFb9X_UH6_c{@!z}JX;<2yGNO)DX-$*Z?vpvI+=k}h9k_A@J zIX6Mw&2y`QJuw0yOBE{XhpZ`nmvgMKUo}ud#I~WDF79LtMTLRyD7qrLLNuf_yEX2Uc0zOk}H?k9C#q_LjLG1Bnug{E|tBg9=q{yx$0Zb>HFs!*c@gRd=7@PNdS& z)VL*;J=bI|eE;6cp%_lwv4)2}HN5NTs`KU$6?`2Q#?uhnFr0KvRbvwfaWFn8TszE} zjAHk?{%EjpUqr81@wQavC07U!4PewB_(PGD*z<50AQ<0jzg=p@zLE-WL^aF=(eAHk z9iT!0k4ckJrMa1M8t^G&S;QyQCob<@Q~vw7=s!4^>q>X|UoL=vu4V$sGJGr*r*R2Q z40mVV0ywI*Wfg0oI1MpKF+DOj{4FIWh`JCD;@2VR4MM^J)0iDR%xzpdd@OiC4AJZC zhsjpfMi>Z}q()=SQF**ZecC)~#Z{KhdRk>=Y5hPFO020|H9I1DR#ecp)CEzoJ$r{u zt|RU*-tyjxUNNty^2JXRwEw|6|Ng6i$0&J~+Ybr~3PuYv(W1+_>)6)w@%k673hOsx z^t~r&b$X9Bh7RK`AB@ufW_XhkCTyQuP#qXnSw&`I}AEN1?OAH7{>;v_wb*wMNs(@S`WkN^i~>%D(B- zsG<9=F7?+x*b0Cb6wBaZSZ!T??}7uHJ)ib5RhrhId=JB0s$@ZhCUgK3|>PS~XS0gj+T34!l-h zmF0*AR9(lh89}Vzp^)T}{Y-{MM(O+V9b!M?Huhp!h+gXe!;^A)y1?%SA+!${UnS8# z6f-DN&!8pY9<9y}kW#1V0v4R+)#v84ht7%=g_#z3n8lgweMT}Sm9^tG4$jO<+44Wj zt^e1lQ?$9ARGxVd$WX8z->R&~5u;?=ncHhnrAJF#-l5#><}cn^pzaj8rRRoueSnGT z?1tuj*5KduE?sr(YmkPko;Y|WpZ>x5_R5W`!C}`sdaxd)N?23~7DM7k=RMytu?Dym z9a^DQF^IOC0UEWE$wD0jxc_6ruUJq4dj=X`2V5>=O_Rnuw$@E7VdSU~K``M4df&}$B_FHS1ge+r6juS#fB_4}n?0C#w2Qr`iuBmH(~2)P za;3mv7rR@p->UCJ^Mbl?*=>LEM4WtadQx}JOYvV>PC|h|cEjV9y=i|BgIzr=-dJXZ zx)?N`TQ8GGHd=FD&98oEE8w7?C4c%W2<4s_V;1@v9;S)UBFzpSrahS&&cbJ>b+)}< z8uz~?Y4p4f?~Fa^rsZK$gt>DaIbdMG3kvE-M?RQ{YjEcIt3qpcQzaO?=^neEMOJ{` zKaI{geYjOnujA`LiHCVBF!WbE7I#FZ>A04?H!34m-$x-v+4h0IIOCV;N8NNh)$U5X zdHMN~dm8wfxLA*4#{}O+i__xvSpi|c#3SZh(zGSaUs-BL*oeFo=Z})NRA{y%Q=xyc2Rr{^FIyns15vro^I9j>c1#DSplT+A094#+iDQ?(W9!!p?{; zz@E_JV$?H6S~Tfh#4^!D}^XvU}|^WH~=a2YOuG+ExLvP!sm z=p?^Gm;9B*{$wB;vDp}+rI5Q0zHce=0;9kG&;;5R;6A3P&mu<`wQ^e6q)mCv;f}~MFtHboA66#>-sgKR(jQ4 zltPnfEE0)@B2ipSD=VwIr%-++ z-kz0~QsBVGlvQS$Z84bbT9|h0-OW(QalQ3w0UP=2>*xOBVF5n>H9q^v4XM&NKxHe8 zzHj@}eIVR2$MuI+yRYG4wDKDysSCbP=C#K07cT=<);^)%VhYLtqU&D_QywE;R2tuE z|Aizf1UQK9cLjumq=Qfpand(+-n_`?wKmYv{6xIE=ETcS0%53-*84+0LsI?M7ovk^-3>l?+-^?q$FMY5AJA zPB>~PV}f(*7&E`w;2$1hqMW`mQ96+O!TqJ^B()t;n+&7t`p%w?3V9ga8Bxmvp~w^+u``jzs8JvNg(dsnJe%LGv+gH^{dL&rk)F^)*j-@qzR2KWl7uv; zX>h-jD+8k=2?$qC&{AelcZ4i73mfL}i&Qx2n(X1KA$9C9o(1 z)PVGNL4jJ#D|}>ss95a7Tz{@UmM<@WYfi!cfEoR_!H_TPH$<5P8I_MYXa^TF%}GO@ zB`|=?n{Sz4Q*UBLOI!>YAoH-bXv%KwJw%1*cXkq2Bx~@#;}WlwWg@+X+ueW&^f(7} zMV4hN0L1ngZ_QQ;ok~viM9HdaG8%Jbs$iAsI+THTF919TD3IU*dsnW@uTo; zNS!r(pZ+Eb?;|vGq3skdIh`KFtjvin#Cy^jt6zmEjmE6v_gjZc;`hC~2e;cS zwGg)c0;g2DZQ?2>bfJ9U=dQ-$?6{HDkWv*I&`If|@5QA_-hQ^W9U<3o^%O1~l$%+< zM8s`1YMp{!$xON79jj9X->+$T@Ai9%&Rk(W!LHsJ=y`PlT<6vZ&&Z3}#URzX{4Za; z0a5bZtXO?#k;{bx+k7tdv7u=fs$1IEXv{@#?1mIC3>vt_jf`*)J;o~TvM$+Zh33&zTC8y;h{{xV8Nfj1&whax1{wetyeJDiz zX_fX5xvGHB^^l36^Kl}dH>nLJk+ifnz-qO}=W9QwJmoR6IEr_6g!_rLUSw(NleZwg zb1_3L7QMj*z>-oO#8TCqUp`5{I~*Ct;E`DUfT&tdBRHo&jtzRUK@j0qx65DQclL#d z!}R>DQ_?u}Yi=o-Uaass(7V13P;`-1^svi;V=YDbWKy5pb`3?ZJazEFN_HHjv+aBu zOG_sxgg^^F10$-UyK0825$eFv`@tVdO-o{^X+pHz3HTd~4{;vSsOm2uy1`teNu6t9 znPBNGaddpaqrH^rq6MC+PL*u@sNpsen|TD5;>C2HKBzvI9D(W~^ITWj&99hqj@5Oj zpK=J&uXtFdNv(Y2dIQ~Elq)m`?*K?6C$Hm~A?-nHaBOIIbx*ejhx$#oVws`F1$=bart!J{K83Dbs6vzoB}lUr?VqImWpnjBJJGBN|nN z-^uJnqb`qi=Eau&jULD%$$8X)hTNz0qBCraiC2I37A`Z5Lh@oXv6X~qWqI~!*?X$J z2suWjGE7!i02MPGQsrDbm2m=_k)b(KAQkLxre-PxfOSQYw6VJK!|d&&BW!hClYa+S ztqb2xzBuvmqL>FXa+tA+ZQfi>O>%#7Nm`A=3*tafBtrd(ki?xYr1ggWJR-&Mqi zfRIU@GVOAqH(uZ{=WeD7>zGus-L?IO%ibiHHn;j6_W8FZ52m)rm^G~4Y9Xa~9sAp@ zrPZl_m1DShj43d%Q*;IUIx5eiT0S5`?Rp%^Z#EOia6NEhAAmuzA~nZ8sxP~FXA&f2 zLqQ=z=BeRxvYA_WkvHSqPKeB%w17b5%oWm(-n_Baf(%mBnG9n9$cnLT>~Q!ja}446 zG*3un-$_=`*$(p6to*uO+ z>9v~)8`I9wRf8v6fGK&yIkOMLa+2*c?CN{{VfMpw8Dab{&JVP^yIA2$)|y*BxX)a$ z$KDCp+Mlv=1wl1hgieI!0UTOZ68Nb9B$ACMn8&|>P;P5(6q<)_bE zx*xJuG^6g!5fE0mZ|~C>wzKUQqqNl`;C}ZGVwAnrVpQb$N8z(x>K^pY0L=e$Y(*#U zn6ezxr%jPs4H?bJ%Dv)U0Jt|F5JqUDaN!lT0rciKU0s@qSb&*wF9|cmXMCYvj)VSY z1bemw_FW$=OHb$*vQM66+1v@Cd769uUBTyDG6}z{A}3Pl{#e4wwGe>+!(6%+8Z;2Z zqnHb>wRll;B3kU7G}_6igQVx~+i)Ux-)O1K2FpX(#^-VeT*Xj8{N0@*G4iY`3Exbi z<{0R4IV*3ROZNQw$f{x5SRDMSL3}3OD(WUYMRA{D5%>@aLq2yAEQIO`xe2IS^_Q-%c1ggfVItiHmp-3pik zz1+T-YD^&YrKAY34ob4>Zqedn44Mn zWxz96?JeeMwQh_1R~$1p;8`ZW^gD-s>Gn9}&dZdxH}w~#9(`)ca|SZ|^UzQi3Lil_ zW*HGtcFSdFU9~hyd2_*p3v!TaR8Hx+j;&W7=S=0^ACTDrA(Yot01sw=&}6TSve9dY zakzTLMMo>9;?^TGWzu6$UhGK=zbV6o%LCD|K-J|>yF3lEjK_W`WUTU5(i64LN8VK? zzwyb@-QYlx&D3a%27p3J;-apUTo5nD>^*6KJN$^e3~#WtSN@$Qhk% zPD==u*vEFH;*Kz^w;;Q(XfOs(^H;K2pTgNrsc<=u<0s<=1sYs_t z*F;oqI+X5m3>YxFHbf9mN@7E$q(vCbXpk=HW&2l_Soy} zoaHCos4WS?#}*#fmIWwJ1j_SM2f~(PRDK>^O^>OrIPy-QTSo({ zg`;fCkqvusm=m?_F)&nNs(y>o5@S=qJUJ^AUgBC$I+9t2Y>*0@KPJwTw?7rD$d ztr3ggZ~L@)8_^-V{sdL_#j`G|k(Gz)L z*{f*s`<{z=_f*jQt=bQ8S`pnZJWrGZ7+Jk>-wH*dI<=q7RrzLwW<7}>3Le=l>tbEs zu^imk^t`jAfJ4^%yC!`Vcwe6%Y6VaqYQ*!tUOuilKlO$ z;cP3=mBi14kg=6owaCL@ISifTT#r`MgVp>^I2>CIUZB~a znlN@jkyfYvNf1<&R0;jXj!ShiLW%YF1fOd$0_wg4=y~_gWk~sl{4I68-gfh2JyrIN zA$EbTO!LF*Bc~?CpmOgwv+9Y;B_<};HP<|CK-%-FM~(VwEG(}~1^n|&Pe+$KC4cuX zw8cO6?fbH8JLK>M?XZgPIO$Nk3H3)`@E@}OeC(1?r0}mWHU3vmb<*7ohcul0Bu#7| zz1#Bs&K+8$Ze5S`N-|tBTV*|$nWv);DX-^evmN>+z`a1J$wx0z@tX!?UY4*kf7$fs z)~81a)Z)tgTfTb+K1QCUZ%=v)OKNcrS9BddL_;Vd>~d-S;1azHL$dEWB)uwaenQ9k zt6wD=cN8t1-WEUi-`m!yr`1s!J;NL%v((eZ%J=3+4Yb5?xy9akxx`$4qHaMZ`DDlM zlMu^02T;c4Amz=8rLA-^9>OE0&^wob3)~zf1|HGQR~DTy&|~?>w&5l7zMot4gtCvD z87#ecQ=8rl8_+2DoYlLgS)k|B$++&$m;3JLW!vnX551eVYV+vIW2Ey02 zYuZp3u(pKerV779-G~86^IY0o9Yvz3FNQq-v}T95`1~--=aGAK$c7z(<6$44h=|a< z@2aJ{TKhq-GJ~3u^tg2y`m+1!lDnRDxXL#p3&=+^t)_2;lWZMK0kF~}G`d3JwKh~$ zPWR1=Z%)HkX&C~ytLh62k^6|3<8pkrcoK?&9;FwrxLAa^*gy^a1q&RhkOGcgO;~UL_h3$mqj@MUf=2^8T z%RP(n+uiLrZaP*3Jc)yutIBJB+9uiMo6=|}f$tRLY%(TdHymrh0Y195N#cC@=ZQj` zCjKj{P5sWP-prM`9R3Fow+?60MeQU9d4_Kpi(cxOjtwrtJJF_-Pt_Y#ACd=ZO0K-( zwR$j8Wn{GitL=FeU|d>-HC1O5iF_=>cQ~hX+3>W(sjYP{-bo~~r$>KQ*fke!=mH$i zOzU#>dd_R|u$48Te zar&M5HREk2J~tcl+kpl}wJ`MQP&Y%o z4%n+{!x;gm=TV~B2lwVHL8Dn}7d3k{qp{b>galO$vo>JMOOuXIIu^dduxb)~^&)^tTVL6{L3$Lex)hN72EdLyzE4(G$NG5R|a{SSLqs?e1w1a0|{9FR>9ad@L$PoZv!-r6A(ZDm_}rP^z7By05P zre#X96W%xPxgcTetZ(D0JD(Y2%)9$iNvW5ozbm$iJa3&i`&}UV`0?on@kR?YbBc|} zYb=dsVi~0iw*bkgHCA8T6ngf@aP{dMY9)7oZ~Fq7K~mPlvHsUTUkQIyf<0d6YabeZ z3*Oz&Fnp|98)5(pqsfTRZJ)k}HPuWn<_zt1bWmgF&~KjVY%~%8*<}|S?P*r+p;19l z=inQjv0wJ}@+X`QjjR3gS$*1{z|ys1hTe$ho2nVZKmM!Vewn|qlH8h^q67`l4z>y= zdTR%ZUr7wa!Y6aU%9CGjSo&epFf495zZ(I&qnMn*re3HdHU%>|3S4PA^)YlMR6q5c zA4p9*^XW{*M1S@4cna)~Cr;P8ElU$qzSb644R$vkbiO!+J`RX1|Kf}rTp?Q(fVeg6 z8oEXyeSU*$54B}CeI1T_coRKXTpDUGXT>&o2Dy@azhSP|Z=$M?xq3g=zI{SF4QK7u zC^~t0I&^Lw*cBE?@ghx$n1Yc)Z*34C>15d(yR$(oo}PsB71IDxjoll^u9x0_$;XlL zC3B!ZO}?6w!koe3MKJM%c94&1m}Bk#V*yypJM@nIGf`8TWlq;a{&l=@e;{z!zwg@V zD-%=R>#+EF)yspL#^qx`>WQA0MN?`Vk^_IOD*bcIGG^|K{lN!SCgrzZD;*h@3wvIB z1UFn}c8e}wY+qWstctF;6KNF8?;7s2i3@+w(l?a}Mo-CS9oTc0{wfrH-MzcpmGNRS z{Gv1$NVhFK*_(u1(ofP+amaS}|eow8k zuWFXNPpR|M&(wq&y|NIw9s62`@2AgMxSUTjAQLxlLgCcE^K=9>{JkqR+0xSaHcrkU zXK4YeFA9pY69-e%jTxfTS$^2n1P`r7dSWLQYHAM55p`L|SL{`q59^o3Ix~rsXPYR8 z_yf&z5r_bHiOCd{q-=eJ+B5=pQv2oDL8By65UkduVg^8acalFpZKfLLWXEEtERi>@-|muI=WifNKOngx(h#h7r4pCTaX zG+PDktn3#KbfrHqZL3=5L6B9Klx)%wDPB(j`wDaiKxt~Hg*Lv zdU`_eT9sDrY|S)E-Gebk|5lN{lz)FrVgT}n^OY{y^Y6~L(#pS9tgO84Pftlv1X|)s zWTkLXL(}Q{jO!XuhAQS zPd1h=N*3iSEAq%LEGdMS{BY_>u+c6u?U6F#vg($*Oq41}t)OP3QgQkBz+$@!e5Y=IAz`TIqBm`Duj#^7v~)ml4C+kz#wW1#5Vm zhCCjlS(aLka}7KzsYobN*hcfwkLqpvmWZ_96?;^+3REq8g?k-o4} zbU3ANA2^R{M1QVCiD@ z{d9T%zPET~&Zx*U9Qj!0RR)VK*wH9QJ+gYPbymvfeaY*EBUy#sER>D>G`np1oM<20~n_Gwqwr~op3JiakUcBhS7W(qGaaaiUQ>l`Q zC9Zmmz_>EmX&^K!CKUG{4y=ymDw@3v+?~VfSbuF2f8Y41@hW9&>C_!czS5xU$0}v* zd(LH7Vw9BEtd(~^HebvN6N%erNAFFg^n;Br@T5BmkU`=Zz}9=+d0xrVH?3+muNm{> zS8ojw~e1Okk5R0cs-0 zNZ%@*1dD}bQV*i;EEci6+kdU2xa5=*9GxrSJL^E!oK%idB@OmDDA_Mj`X2+=u|`n9 zUFg>g;sNj0Wb>SaExUuJk2R4(<2$%=fl#iw|FcPmBz*2GWvn&3a6!YVq|aYIxY4(i z1rXOkIc~RP>6`n%gld-2A4bgj>@MBZdtCn1dqaY(Z(m^5HcEH{CJ5Ee$rUHQ*brEL zFG$aE-u`&WA*v%`*r=?Es9QqMvt|rxA$1YXb{eO#^#X_Po)UB{I&GXY{HZS~6 zKIDcNwTqK)$Zp^S3X3+S;3y8ER|Sb`+{a;K5!LenU5YzH*>eHpMOK-bdLoXVOYF}% zmKRhxG#W})d`$`(obbwVWm-<5425mCQw+9Z>#8eyuTL*5ZWvvtUzALI%|)0B2xyoO z6RP%O(We)ZML&ATkVMV`QI@UA~7UdT`vPhRey(=y0-N$xkcgUrU!5>aqZe=h*AaDCs%#9WE?U-bg zaM6iGahCvL%wjE>V2``8-t*Z#?&_xOqYj5awgHeH8h_2b8ZxWNq%nA#wi3fN@=BP1ST#7y!eLHuhT z*x|}nu&jd_vb??Gj)q?wUE!*a%aAEhJAng_QaqCT)fEHa3a9b0HjAwoj8vZ?tz7bs z-}P(gvsDa+?97e(waU3I#h^u{&CSg%*3J2`(LVPP^RTOd)WpTAH*bzW9HQX_=FkDviaBL>NCdhp8e5VrZ;>?s? z1vN7kgP9PXtySBMvnhDe$T`EV zVTy?Ex4yb^FY?vM5;t$y@^{#inYFrqxxysp_gNZSJSm~&)k@f`&o`nUarR-M9-4Lr zoiQG41pZpE1PQyaZg`aZbLOMBMJW%8WMJE&ymiNmaZ`i&{2z^Z%QlZkYphu|B)m&u z?MnuWcFUtPxt;J=m?<+=!bdLiA!RnNrtj{I((Vy0c9JO^)UA%snS*79ZcoQ<1SeS8 z^18|bsp6>TN&W98O=i-!38qOgLw_OGcZnq9uz1#bvSu^^XQ}rhAa4~7)75B4BD+N5t+xkB ze=b`{r8ABdTol_cDhKwzsnkz};q4x`i9SbVKIfcdx0IAlc2t@h{4wvGxDTKF2J)|m z+xY(ne;9(^o;tGomhgV1yQv;uFU2a;2*$7td%R<&H{&bI2;N%HS{`9cGt3PuZM;r{ z!ZY0zlLuz_e?pV=_TOL6T}?A?D7}hl7&ulNVjrb>KlAbtuS>>1Xek?cIgwOFGAs(F zn(^n1>!@MN?I<{1gaq7#Z_X zH}*XU;)gt>BK~3AK8IAt?tHpWBIP#pvc>|#ZF=Fn(W-G8!e}QrH|fk67bN(e!&cFu zxnb0#|MkM_FM;mGX z(Gd~XN0Y=D2Pn2`Nu_+StPR*#xNOliXT;|}BPW&0Q<@ix=WKz$^2sAgn315$T;_e*jwHV$E{tA@2KaCMKAC8L_!NVd+eDO2Bfvg( zB;F?!)An8&5d8e&duVmpot%clBl#@?%c~7gep=7ki}19{SE<_8&+{JR{7(pLrChPQ z6qct~qfy$L;maC(w(yHhYjWn*p9g-aK}?M$r{Xg_??hivE97Jtp~|n{oY2wd8pyNZ zjRPBR0**1kL92JqZ1V4~J_j~?yh``_!+3FV5f}W4yESC^iYcSvl2M1Qd2?3~go&Ud zH!yQo$2Qcj3K9c#zIvZFArK$<*~pv);QFRsbn6d?HrX?w>td5FuP|;_U?kRHM zZ}pZ*7G5I~tVNmu)zjqn+Q~Z@jWsV1Mh7u)Bbssh8R&rxI(8>%JF#f}f}8%fb*{y1 zqi@V1^uE{^UXJ=7(Q7QJ(3Kt~9W%Eo2m26htk4I#nd1l!|I}>_PK%=0(m~7mv!=CD z7S)%uA`lL#S@E|GEDBy<7#PG`7Dp3oW%snpq~dybWi;KFxW4~o5r_}@NSv?bJUO~I zxR&xl&po#x%UTv2=l7gpu%VPAQfz@t!!%h!iSKnL@-*JKw`t=W?x3=MGhx4xj%FuX zu*7pfuEXrt)VXwuKjjyn#ctC>l+aySp7SRm0En$kK+RiT`h>+$fQ;Fta3J!jzm@+5 z8QWABRBrw0F-trhs7?IUr5*YKXJ&VK3!&MaBY5v)wbXYTI2%vId;rekv~*3fr1Bk( z>|nObtm4_c8Q}J4Wt)_pDjeR6bZGdy!y8b-jX7p9dOvw%t+p6hqYjrZD1K01z1_5r za)e*)SHYm(_)Ab@ZE(q!ZBoS;A%?@KfNQwS1yB4Ixm}QjZZLBOI|p>k#+5`NcVeKP z1kYB15aL%|+CblQ`9coAOV0`M0;57+D&4VhypL9b>!_}Z2h0}o2zLHM(`gxdV3WMZNwt~+imMl2-tCB{`Ouy zb;UQbn}!)nGC*A2VIryoiq~(&byfS#O+Qf;wre;mBYZT4jU41_rfuK=N(~(+qlYRb z(QG<;mv!T3ia}dA|FNo~K^b|G#JG|GyG%p&v)GBCq&nd$7m~-w=Z-MmQWU`<2zbDU zNGy|KL%*E*iw-i{;iuP`IV3Iqe|AuLY$)o2(9f3mvHqY@nr*m5`0T>B*uxLoaD9hr z@A5B_leQ3NCWFu_Q}U|8u_C_nj~&7u$08(FF=(jhX%_S(r62dyNxL%Qjbs_@q-W426es zD;k@%H(W_I+CemNetF>O$YKTSsG=53ZkhExo_g;eX}fS(2bD%27vO=)?C8}yX#TZO zun6M9;j6N5)xoJ(>J`s$N18lYccqK=(ME>)I-OdgtJ%Ed>^Wwc0K51FtE3&!aw1aC zU$yJfiX@ct9jnkN{t~WgzrUr-~sGhm<$PSdW+Aa+mWlyU{}af?aXg_*|-=-kDPs1Dz}rDMB8G@@z#?SGgo0 z+f;(i37K@==4$oD38f}}!~Z(`lT@r>{H$_91x3vz%@=a{&7jddtffCzay@j^nA**=X28ag8oCro!cCPBoofvj(Bg?eS@O&k*V(cw0cU# zOhGD1l|n76gI&AP3O(BT7*n#d1I4i}Pm;Xs@gqw5PIC`OHs)Tn&^|D`kGscC@u3Lz zl%z(UXaqEO^f=?ajYlNnOW}|OUxxa5e zLZ_GFZ#P)EF?&0=oCTBYf3jr<6UE^7Hzb>OX7j)|+9R66yd7Cc^U8J)3EFeoXGq50$lpHSXKB>9>t&}?_HKuPc^Z{dGt0vyDM7? zmM)E_VW4Q}R<(~WB9VwH2de{09auf8@pYvzdO=Yvu#W=z^|iJeOG!6DSEMG>wQxQPS?wEc(^axbg;z4I9dv5q!El)^$)|6%l>&okNx0@ zpxjV1_H|GrOa+{gY=^Jx_#t)@Qmh+|O46+oyG0NBcy0;_F+eZE7w+csyI$zJ*p)w^B2>QJs?3xBqXYj*imESo-riR@0OhAIL4j7C4Xx+BOrmgRcCk z4b9;5!m`7b%YNK>SSU0M%GR7UV#4oVzk1M+mwXVNNTZl_g7+*=mAm`$L4_Tbyo=ke zfkDwh7RZnG7^wz1niv+QJ7a2q!}RTqCC88{DFF(u78S|A!o;n6hwMXjrrPLVov%0j zLG!PrQuE?G6oHSvf8DdmHAhc76~pCd5dl8a1PkwZP6?7aX8BM zX8?nuO4VbwJ(>vL&rOA*4UX3JFwBA~`)V6!93pgu$wmN@Wz5hM<}ZueaUEI>>vb-% z3H@kEiY6Aj^wHf_g&S6x1+lZ>zWS^Ed6w72(&B?|12iDH|W`t%iQ14dwvF1Rpei79t zxuU_(w%c?pB}nP7VfS@RSAWJ+r<>W@U?x?^zz1x|Q91H0tA8$KdO&Q-@)ct~7Th+8 z%M1R012;dCe)YM*Qrot1>rWe%rhl7wdRDw0g!>X2CagSN2QZ7x3^|QF97fAcgbl$< zoD@ZbU%~TuWa|7qVr)rLaQ=twVv2}d33oZ>Vtg3rx^eX+b!`jH`w!wIhsKYx)Ug@^ zhgay6p>WE@hy;UFT1s|3j9e-0s_2%vDHru8$+aA(xz-wFIoJ6E=kAW&jKkElbIlY# zHRs4YtO52P=k`q6pF%?qpQ{eY&0vKekEmev4~IFY~|lSc`UM zmV#kKWoWqT^eraBYkHk<`C62RghT1M-!>!a7G+{4=j-1iwZA!Ec%OB$xPc^YqGkNY zLK{mq?d#Ix`m&LLZ&K$rzgR-k<=k^8?;3sU>$M;j*k-}A#|Xuzq0y*|Fpsx~4B@zL zJEU26l)kTXtu3% z6VDwps?GDvMmw!3aI+rVn^Z8|t}{T%!jM4)#17X*;!JRN7HQyAk}wB*Y|s7b|5YnEKvdPTz@+P@F1wVK8mnMD zx)U_2--O7Wh|9r$LAxHe2#_k(gTxL*3aQV$@q{PpK!uw{G(F@SgMlXMwDW>wU48^W zZOuqX#^xZ6XFg_CEffjGlTGul)qSXY}Yc_(6Dj@|)V z>i-d4eK?eteP&{>-wZK>=FJ?drvd`0v62 zhFJ*-%d&th>vf&2mf_t%#x5uma;WwM@O`H*fHQ%+#iGWc<_r&h7%-Bu^K`1L|RG zTye2HKkIW41w|&w1a4Gc5?{|895nonp{eSHEz4_N>Pjk!v=`6qn5~+#6UD%$Xc`2$ z4ILHaPZT-+#nh`Zws#c@Lw8?06zQ|rb}&OwZC9r4KboBeY!`I5ruKW5g{Q)6lc4hM z9|q)j?2v*8vcr@PD^o6!%?4!zVD9pBbq$d2Dg2|4k_52^X$mgu%^~HtMRmP7P1 zjq{6pkpqngVH9KJo#xgyBN*wUe}0AiO)4tuzkrBfRuEoavNe9# z6YjlzdRx}n;!_~|dR5I{mH$6`My_>Iu3P^B%e_ReEd~6e> z2am1?_B5gAVD!Zy*THg*##zcn6$>73jEjrlS)rJ>27va4on~H@H zGo0;2K#1ievbV?zZg>BAvqUx6@i3#3YX3Wtw>xDFA(VgKc3=P-i^x$EE!FjRAr2ix zJ*oD-z8>LE5M@1AdXq~M zJ-3@LZKye}y%$_Y?Qs@$g7+Pl#&8G=U=kcr2EPt{hHif2(DyhN-VI*5!z3W;aB~0z zFxdI|f*q@;N#EDE(lQ|m=Rnu^c^ibtXgp&!+@@SccY;SX?cO^Z#YUClqC6xRS1A>SN;X)eBq_g*?TXUA#h+qMXSp-s>hz#iN|?9TK~$z&>4HU?n!-7 zj(DFFfg0H{L7A?qgW|$= zofLnrC|cv33c%O9^G_*iKXfhZE4NJ_w4M8O+4URg%l3`Tlx^SKDc?@2lmkt_Eua*5 zom+(5%P=?@@lY?!Kyh5d1w{5836{nA3xZRethkF3Pu7-};s7A&^oyr~iOMTPI^!jP#{D=;E_KL7-Kn6Z6aJs@h_?8xQFL38-9V2~W=_)Vu_RgaSOt*YRcjioRz@!|MZy-PQw&d7>A{)am)iP*SP%F8lBqU zBbN~_h8Ly%4ki4-^&g`DVHrRacD@#w^w_s^=r*n9lwWx&CLMFeizkahezOYG@HC|F zBVA1^1E$3o;k3T7E=VFhb91hLebOxPXHoeg0-8dDsn5KrVivy8rI)E05d7Ak_svrm zMuvaax8F@~G?8LnaJH?cW=AX4Ch)9O|#ZbFOBxDd(P1jv@T`mGp?xnlLm zlGrv3WUgd;^T!qM;(Yf07W`X>EUsDXqA`6-!bLnJZNy;gR_ZyklkPI}T4&f!^ ziK%-6x#w*)60s61hEjb-=#Zn-7^|=7x)7Y&F31>+FUsK7n5dbn4J)Z@@Rqz|7nisJCxmRu73YYrPbiHRvF48b-{X&74HJ0L zd0MgR^3CTZ9Z7<6)1^L&=pp8(yPCbCH7|0oW(lq-5uaHlJ6}7s9h3?BojHJd*;{a( zSqC%Gn+SI>R|pvJ&<@!8Vdc`NHlLU202*{qo9wkF@sRo4g1p3iFkwFwt?8b-)aV}_ zHo{9-urba?Q7p?7ft*E!xqlmqj`ZY(eGz`5{uiH@%^kh6S_?ySt0?VOX0~(L(Dgol6>@958Q0}-6x_HP}YlfaattlE38x~OqCL;ccTO7 zxp^!7CbTqA6|H5amdi0xwbU>FxZr&`NRJ+DKQlagCM7&OxcB6lX$GywOl}ysK_#5} z7_6H{`>`)kT5%irhtk?7}B+tW6jB-&C(>p0A#lu zojlP1M>>GpZ4y>H?@6DjrIW0sF$fbCve#OfXzKi4gC6=%FMK}J(Q_`P;>X&BGjnVx znBQk{GOw0PEDmLUfU8QZv|O_q&fJg4RS;v0^iZwM^Zp|N2H!w?jCw+N4(@3qA33ew zT79$SajQ$B(UuCL)i&XrleRKOpl%KOWFwVo+Vbv>+)!>}dU~$$)<;jD@3J5!Ar|W6 zd<4xcfNt=UAKyWy{%u#8150wCTKQJS^h-IX8c*w8aZ*Vp3!An2xO)6h-mZX@uESTK zgC^7WFB|>-w5US&_yEEx24lL#6C*{!^ZGbD?sXptuDt)41GsZLaceWBTts1p023dDQ>*tl8#>dHI2v82 zyCOa^(v;aBFCG`0B66F$ zK4Am%gwlMl<0{z)iM?dIq(-6VD>9Sha_fAl%4KoYLlBr!S&2klJH@2 z$bK6qP+U;dF>^~QT|Xlkw9YlRwUK0@FRMaskx@To%l<0E8AXkjGa1+)*(;>S)o)#7 zCrhsTCmorWoerPpfuD%lu(f8#c6_+%{F~n-838PM90u{`6YnH`MtIlIJjs9OwK&pc zESJt;!Zu>m!B^5S=~Q)hA^s@%^4PY>{4?Dl*+5f@%GU%!s?zuKmZ^$7j_{jyl?Klv zV2@Yw{>(E2$}R**>pAU1&V}NnAgdO;7_NK3sSA*s|ggoVkQ5is&)Y z()d9(FL0|7K*+SX%DtSk8i7(toZ~etu3RlSWT>`Mo2ohS`xoExQ~B?1@H=CBNQ<^H zwymU4fX&C7l;X{Cx!pnMrO>MU;k)Uwp_=o0MrcJIkOGl)j znsQ!3xG}fQ!)*K*CU8Dfz^B8Ek8H!9!nifZ_uFXC`%3IWrFnuJV`THlt5kts3zR}Z zjjw-$xQv?zo^u%r3%5&7;O%pKT#GQ1)cl(V3>qAh2I00;7amiN#G9T$(~PU1fy76{ z)X_g|a}^oFRf#bv2X~0;Wz-_d=|uoxGBlIsw+GMU)}kM|23|T7d3k5wo3LIEUycR?IH2i+3+h4o*m%riRjwP%m! zctILQP6n@&6@#RFelai>8jNWe3=9`Ow6*9&0ttajb5wG70hnEev^Z2IxfYMbm$W| zyPa%$L4aKEeJL!ma6P_Mvn{O1MM`HzyL#2d*uf5WtC+VV-=<2-zKPAW+mDehUJMn zn{>`T-QabNDpNQgNrgu6!f9Ibu@8k|jp2c3NuvY1-b~w>GL*U-M%AXrjn`h_j9=$^ zIAW%F#=UX8q%80JhCH3ao8JpdIt@_}w`m%QyCND)p_xa+6{a<>3;k?nhkf-XfT=55 z$quUQze>K0N_`Pk8I_`JgA3{~#<3@xqQ(XK^l`H1&SwJ%P{KK*GzxqJ>^(MCo|9XX+i@yC2FQ5&rmz;7Yz6mWdTh7-r7b zkQC2>nbkW>E&I!KU#*_WlW+6%M^)|ou-U|zPZjF};FWAuP1zRo0&z~~YT}M2h2}n% zl>16w?5Yk0F_W^!XRx6f3B0Wtoa4^uj8Dg68TRLHQePG3+?Ywd!0d<47Y$~wj;ghy-6f-F$&d2+dq^z)|HRg7#YfJYWIfod4Nlw6=<fMTP}gU9r1%VNO9KfEQTT{=If{j^SO2Ym zD$pgd=^IsFKRd25f!j^(-*F+7cJx1L$`9(RXIszY*?Vu3@^}?~G$!0~EHpE;Gg}pq z>8pP7!#}}d@-oizMpA4n97j(a-O^CPVm9CD$MN2q*;;WvWJ2tP6??o@wQmM0FJe0* zkTJBFMOGP`9To-fnFjjsOQn(XeH*`q%O_>G4c(;_(;k=7#(kEroUv|8(!bcDj{abkoCQ6vnkx23CM!}tR#Pf2S)q`f0NQqd5dYHN`CB2(h>p+ z1_xxm$`MlGfY>yHA6@lgUZw~Xbr`JG4%x=%43&z2xu@o^vJr$45ktY4GE+EJMm_|# zD>SzH4$SdJg<4A|W68#15=YViq$0VH;P*9^nt!CvzrSAMH|&&C%Ek*Qh-SXI{ozh2 zd0K4qhuxn-CK(Xs5(h+vyEe+hu}|Pl>DY;vS+N3X`DQAcm@aR_C>*NZr@#N+Z*j{v zMZ_>B1aR(3+M0RsYr)X5Dk(d-l6=(#_31KfXu#*HMw;fMulmBS%sR$Kk~$_cw|2~g z*keR72o{m({6e;Ds~o#-zWUNxj$UWF$<0(%)3iHFd_Lw>6NV<1U4S;PL(AyEfoVd9 zPsTuWS#gBdrpNv_kr|g>QO(!;sW0_Zmo#Rc=hdt#_TFBf0mfMz^5IolB@ER(ruGr} zN}l)%fr2d?OEK3}_EoQRp-atwV3Sh@umpR7Dw4n;FQnHcTUbdrvAUr5A1+57st@L3 zZxRE?9^!gQ`eOr}v_tDm@dgx|<7@S65s4fr=_}kCv*ifJLq(QqDtPGVl(C7+DbELf zq2t!_DjW5f$?y)CYK~nOXTr)sk@DwLxpDas%hU%=oFlt^qTC4dR6eTJfcF^hClyN6%c~Egk3Zb z|1!`gyK*tY4Dl&n$QV>0+rBvfn|0x(pj5a?#+=XgVuag2{t`eu71g`#l{KhzoW;j6 zBa6Du1EGcJ-5{4YVOa#Ze18|k9nivkF2~Wwz$toC@LPbQb=?SGB*y74gwqevPK^+gii4nv0nfB#j^4Q^5wrXq>1&&=6f~jH`p4#CQ3N z)c8V5%*e3%Y^>zMzNA<>GIy{Bcn@JUv?pK?GTjSkKH9BvF+W|vq?8{VMW(p!?uD{F z$kOBKWfg~;aR_Sp!gj42{(}+iXS@>_{6mb)#gOng6C!#I&O=f|zK$}xE@;dSEfAzE zJpZQ-O6Hu>jbcF3UG2_+k_mn>ol_#|r^E>Anp;up0!ufqizJFpQSu7ijYC}y%B{Ap zu{ohrAYjgRq(|1^dmm?dY5TREf7lzkRFcPz%2$_e`DY3hoHmf+7eSS%J6`YD8HA2& zl(b~AZqw5+&ZI{}w<@W-X4qey-K4)g29j@N^fZ&cJFy}uQgrkL9Do8-_!y7$$%PH0 z_>0kg_{Xn)@P*fXPqe=coOVtl$4+{YNKhEXY($5O|6NPr6qS{lp8A?f`M+{TA|t2h zrQgQaA4*&$5C66u*g4eUO%Lu`4{VF)8@kBFq$~Kn8ALL28wE2tO{zdvqhKAgDSnn4 z`(pO4qnh7>s@8G;799G&`h`L40A}WVdQbOjD^Sm|O0(}ihr|plJv^)|nY-Fuv${0+nh1DyQos)g8rj6q zO7byU13*PLCpQ&8tbQQ6U!5FZAinq7i8|ALs=-ufVA-B|*bTc2mVId93>{DcxNFUm z}Cv1ki9QSamLOh8dwz`*%5#xWaQ7$~neS|0!|8P564ZU4{ij z@Y^(mI_SlBup%1KN7~8;pfa5Lu1~5%)Uc)`v2!o+v5M*JbN1o(=#g z{{LVtqGRA42seEhBxT3e8Oh;2ii`9r8fgZXgq5$AHTiB<=$7PG6)x}!7j?D>|LxDc zBtlMe-Hl22b`MgWy%YFny{D2p^UVGYIw|_Ek`5+88168xRrsBk5ixUt_&M(KvsS_F znwtTVwRw5BP9^I)Nq|@@BizPva$>M|ReAq-WRa{97mM(7kh#p)Gxxt_yi;S}L#%xv zNw<|x@}#fzC>qf!7h{ z9^||+4|x+gUH@Q>>Pa-C06-1wB$MpbMefy&zgN^awrhynn5$>oByInQ%ynjRl$1qc zD{ccHqWfoGGNj|3a$R`-hocE*4@OuL&PaqJ5pTibn)hdm`=YZyQrx`7YZgnkCxN2A zzlS4KVHKOdzdr3^b03QTmyeX6>Q#hmJS)E0;>z^zZLZ%LXILJVx@-d52Exq8?9y+aU` zB2@)~bdcVAFA*sbkRlyIZ=r`4AVA2sJ?A{nd&=|v-tXbBeQ{;)z4okGGjrc-*31?= zf$7SoRvlu;e>#YiXH7n)x&K$?36j!K!hEH-!EK)n;b%%pevZzbqf`m?-hF+orFuC2 zFr<#Y_r1E4#R$1Pg*m@u1|KX@!1)q-%-Jf{xe~_n+(h-hKU;vO8tsyf-a_clikXjP z<;~!1iG^rGpzasfP2-D=qHc$DNkWB{h!f#6m`VznMFUWVVetL8{No7Mw^zwO_!je! zBv-RBG1b9#39~B9oObwo9&g-TmzF8#zRD5vwb?^&SMZBbN=29`Mjv|-Z2eH2uxp14q z9=V92>RW8>>~@jn{kvaQ^m^ZP~PRz_l8fm)Zjo(WSv#3(Z zG#jf1njSR5>}XGdIqO583Rt#{d1p`i|639RJMiYpYG+GD5r3$eAGLn?dmhzww^JtCD$mM5)gUY&Y_ot7lTt6Fz2gIcsg@Ds|8ciN2VLx|%v52;7 zo{Tf7N5|Nl1Q%wdIb-OA^DE;XaFfF>)h9Uk#Q)LY(ZhYuBmKNa;8ca|RK9IvNQe(g zVWp!j7@58ToSjPau=g@De4R-!V^dTbWX#U2SI^`z+m6cOekVD|3)y=yJ~X{3YdVzb zuG;GVUeF%l*FW8$dLPF)HRJ@8Q3*r1CouUp$;Ay$>sx{65nEvgJl4a!H6x$;p>5n* z4o^*fxzJ!0nFI9UPGFm*mjuHfoC zZ|~~dvcd&8=}nt0XmV8U=@jp1=cMp?(z@CRSC9rH8CLZ3_Ud9Yu0{D?+Z2tEl@t={P} zND;4HCT37BL-aNov5KK8V?QS-tlsu9+^X3`794MZV_LNjTW{6XNSv_vmK`p2#y?Jd zlsGH4)T5ia1Hqmj?e@;EDvp%6LAI#HOh#SDp+3cC4ynQsQ|BUVEeh_a$$@@fYrPg` zqp_7&lkgv;_K#iD(;{238RC#a?`iX7pI0U#}Y3_dmVhDPF?RqL^xcS8V}D~e0lN>r)vrz7mixJO9R+^ z*ipQEG<{L1M^pV0fy*jgo+9ai?-&M}7l0!a!>ym^sir-E-*(LwtMMK0@}Zy=_RtDS z?{8_{?*UVyLS6F^A_6$i%#YPzw;w+h!f)93l(3s5oeyxH!3oo>ESo!BP+<>)L|wYB%h9V(l{?|tzK!~18F z9|MP-4iQIRNCWO6(_Eq3z9Cr~(-(KPr>??k?5cBCGY{aYO>PvG!<*-yB6zDA$CEHl z+XEW{?e+_h&)|!ETY+h{;Rb_kTWvSeysSO!1w<4p5fyvQVMlmg3hEb?c+V219mibb78>zhGJfIQh7zFD&ckjtuq;eKXZ}oG;wO@sy`Sf})yd;h5;+ zapIP6j`KmEYqs$x&#cLpB`DHd6BLVv!$51@n+Mt8kZ37F+sb*!TthHN@0(5zP{!v(aOWrcxDAu#nga! z_DtIslyo}^{bhA=_<@eRnd-`@^*g#Ao1fGp zwfwKqOa#d5m``tmQAyC~?wvHJP`XXj9oz8|O0$nTkh6{#Ldi~2oyK#J0JSq7i8C^? zuD#PUu{4;=INwzPd@gDoVi*=4h-*yb19T^SRQ>h52 zfZdmuW|4B}?e>+QfJQZHVK>a3v-ZDT!@K_GiTic0>5qj$le=?fF*7Ug;gp@ylh1@>1#PnCcXJYOuS}+* z;v4S@xox2>KKObgr@tJDJ}i7i19q#m9=2ScIiAXpVWLtKJCC7V7;$K-shnL<_19u_ zA13cVZxG0~9=`7^%{qfkkkLq$**-to_B7tbG&zHZLwwt>xei(8?=UL6m3+2h8 znsA*ZVHnq5X6>2yBir!&@xvu!zMd0K)g`k+FDajo?_by_RVd*6JIeIp99 zNQi zxh1xX{zkjAH0^f4(Yo-1wPgI;vw^K#wBoOyJT-y&?$hz&S|J|h<>_QYi!fkAeH3v4le2 zB#P>4W=7TVC~VNz*}!l`lQVeoSf>@LG|>sul7G{E26M3kGP=#AhJNJl#Ew7al(Q74 zc+?pEhi~G)YH(DZ--*n>NQ4?L$@3izdlr9#8AmbHSs2`I@$fhOoZi|Wf9{3DcIXN5 zQZho1(6QKBy~Ir!3fEX}_Ykx$R*6>va-Lp4mNU8S*$r7CD8t5S=$FA5f)*yBdL zG?Gd1(33Q?97A26k*#v7xRnCwn;OSeUxyiG{aLnloFVn~OM&A?+^0WtVRXG=y0thBtQJ)h^M#pe z%~fzz$n~_vrDGxVKK?;jH-1bQ<>S_JueK|zNzv5srVB-DLxllT`MV4t40Aj?<@cNo z8rz^#XZVLrNAjU)Q6kbH|I1)4FLzShZU4Qj2+DD0rwa{V@EX-hnZv?f{6$x2V$6PZ zX?N{yUHAAjiN<@=5ubvOf%25DCuvl|t7I^--CN#N17O_Io7rE%I&$`)H??hI&F2x7*5((u(c8 zhHMwXXSo!(!TM&toSq<5)O&aFKut#%albE5h=e?rx}Mz2M&T^5fi#FW=bV+RAb1yyuneyr{N(3ciNXaNo80G0$LX@G7w;Whv?&texB*Nk|brL;8r@R}?yQ z9&PgB*^fX~sN~`N;XB@l$N6LM&qNel9rbHFn?+h%&M4O>r%CQkwC6AsH8{kTeAaU$IO+nD78DsULNxaXX|@PYPnn@4%kTN~`Y1A4=; z#JwdeTKpS?AYDB}P;L5MG5I%mGUPh0)6J2*}a^f2$F+qW?q zWFx3?+>4Y(idrvi7wePQhVw>Z%c=#zSd5|Q%3gbu*|D$E*af`~yUlIL$=O3(YA!xjof6j;_>aR#A5;-|L6XldIjg9(>7f6r>yeV#P-LMm*djYmxJOFx~o3p z2~f-KeEi-^Nr5JWGEp@zaFSX)O9or>_mSH8C9xQdV^eNO?zoxJ=y0DfkL1oTTgsv? zOq9Q})k(oi%%#z!x#{}iOz`yleI@zUvFm6dx^e)Ta2BSR-qzx4Y|Y)%;O34><8$U^ z!p+TH@Q7B>Lw=U58;@@DTMw_oAudOH;A)Az<1ncJ?)-?)jdQeTAj0cDCaIcsWR8NxoSnb`kt3< zMu|SX3T%1o-r}-?28_qYCk+>;|No9R1-{(PdwrS{Z9fk-aeeY`|z8Ft$GA`Sm5Ii@*8L>96}OO_twQ)E-2N zc+cuxdfc0LA7slA#rKr5hKL^?*_XvdzmCq-5lbadYyq;tYhK23p*HL|$GZw>U$4w~ zDaW8CZ(i72X*#GV)ww|s8nBt6iJt)i+%X0oO>Lo};dN-A7ZGaT{gD%Nxt!*nVz@G8DWGMnb@uOxv&y{c8l?^#pSthAFOyDrcR zmqyjJeCe9m@fmks+M>&$9(UhbadcjBJ^u7SI>}&gytUQvT7OR7_=nn=hE*9-V-?nB zZS!-~;zpJ0*cRdT=bsI7Q8_8#5lQVPow%ajELOClc1XQ#?@D}M3m9sS^-jq16^3p! zKpmJ7ISjL#&u_0!zu+{0B*Z`m=%|SUvNyiHJ9Z#rehfW2ouOi~rWn*hCHHS8AJV!aTLE>MpVIq;ODK(r)(rZA)4^FKB6_WWCG~rb@Ek=h#Jd z%U!X0SGIy#cxL~^nZw5}fYK6k$(6DkHfKKmCS){ctDTaGN6l01^-B|3`>E<~NX6%3 z2v6D$r)x{oCI}9+EliHJMae;?&{)v$W<{%OCge8*{~_g#q^~H~tTQV`#=fc>iE{-YDVfNvKeNmxdAFY+uI#~3=5O*H*A?2#J zeuC4K57RQzvF`4|*%{@525+>H9@E@|gqsJv8*(2>bw9S4coymS+e_6B?Ijo-KXWf> zJgjM{HVFB5gaC>TFYXKu!o6Iv=s&tS;Z`M_jAJ#Oh;FczbV(Veu zNi+H=Sj1_Ud)&Xl0k-c|DC;hRVY+5%+*Fm2cZa+$EHo)vpy7QIoT7LS7Vphz$%yTn! zAsbD)xiI8VH??rW?XxY)U4%YLjbr2aQ7hZoz3<|3Rx_${D>hqqN-0pt52`Rdl$k?u z5p2M%OXoDY_-o{y@H%QltCr}oWS;`u;Z(ffN0oGMp|v$)9j+->^Eyq3!G@7ZcP;k- zO$B^}a+P=SF&R_3uk z&PVN5<60)0*+#8&MaQ4sUkb1&8bBHyK<=k*4qK({IOiG(6sR>?uo@i^)l&$+@XvVh zkrCafTn9G(=tfL>ndnJIA(2+oygFy4ABMzsub_}a+i(co8FCumz(ZwZc%S_S;~2E) zZk5YPP`jP#EeoBbL$mdlC3A1?j3pUQ#Ce|L(Bm zTQDu++li4&*pL8kx%4~qW4|2mMRGW&c7vVHMw<#>@_fzAmad|`!OC#+^Md$@W`hBG zh`Yz-6-XTUoAzJ2Ok^JOl?GfKXXwx*V^BN&*y4+g#760!nN8o>`#6IBSC_V>iU_uV zW9$fe!e`KGgLjP!r_y6z94ehoq%(wW()z=_yca!T3B9~c9R|TgQ5M3z6`-4#lXjaNO4{#M zP+}JsEVv(dnqGm`uN6V zYI&vfaKsui|MS?(L?gHH0=&_VU!E?TMN4WP%>}@wUOakUDMNWO-h_j8zp&Jkxl0m6 zG3v3_n`xqLA~oo@6jdijz|WqxJM5z^%yH$HM2zvY?}G%DXy~FAJU(Uy7|juc))?F6 z(+~!=y9Kb;2pAbtF+%p&`PbbPg*iiffpDu@Idi<9I)7GkU9&CVbeNOh{&Wj}!Bp$v zq1+sH_z158_c4}GKbo;(rt?HTd&t32NEts- znWN{1iI$I|e_my&vMHNrYk0?f(wxe^%miDK-}ya-k&i1`pOdW#HTK+~`n3rnT-p0i zL#mX0dHO?Sf4?`N0;~%5+Y25V(n~UX`$U^^0-$TfF^5(AU#}ckpzG%DnR_bckuE#P_x5#C;cXe)RdVEHxXR3YfgE_OL)lB?K{7V0UdTJsGZ~W z54i~DSb)tpbZEd_11_U#gqodw8{(Mtv+>hy?7H z)0Gbr|J?&F1&YdNOyP(2nkugJ)Z*{`w-Ok+ zhBwLq(z<)^-o0N=)EaS=(I}mt2Ic1haXkuZf65LLw2wwhmrD3LC0CH=KRd$!L62m;s-6<8h5C<8jwPzz+7pb06 zKyT#WqX1Ls)>}!$xk1hRq@g#0WU_8iRTc6FwI|sNxrF{_Re#cQH{+ly;0!iyestwv zW!v*2UL6it$?i87E!o(_Jf)?ssGH}9=9v9~m7wT^-hQ!~4+IBR-K9-Cnmm!HM;_-u ziMMV_{-bfWmFgEb_Fb2qK$*4q1Q1;hO|zRve!dp{Lc6{~VpELM7WbJ{Vfau@)%Lbn=_K#vJ9K*;j@ zcU_@cJq7y5f49&*{G=wSe?L_;jrT3#wQ|m7S<6?9#$werqngy*pY3~TX{bH_!<=mJ z{L;?4Z4T{y&spsEuxk;;^Xds&JP){(uQRUF*@muCE)J**_Y_pJjoLpHA_`L7#NNEody5*?EFc%Pdq1k)};NX z^Vcwc%IVZW6(;15G>*_}F;JV7AOAw>*{@2P37es0anUrXG+tuIU+=7DxNl0x61=nV z=W}p<^#mz(dX&8KLjba%x_vQ;ym-Loz!WEzVuJ9%t^vB zN3%ul>fY>bFG3$I7+){n`bta+ygT&vkWL7BYZpYTWX>F!98;CR3%(PZ*Z+ZvQ)fy` zGzDQ2GAG#aBX2b%whkGCTUErMC*B9neNNe108>KjaZEi<#@3^ z8@7zjW~R{u@;=3)SQOxNM2KQ-p-&`tzh1GMFh`Gx%px3MFCe%d%t7_SWGy<;lBY!0 zFLjzQ%flA@ju)@Iu~Bu!uwRrEl2Li`z)Zt0w=P6EaLnC{K|)* zXTyO&XV<-$ z9|=HZ@7}RHJ273`QzdmZ93a1m>mp_CBF@rR0@JS&jU&LN`VGLcKrf9?6wL5{tN@FOJ5mSpdUk%j5Fbu)>Bo5GY+FpXRrr zF979;BS)rjJGP>0&lfN>7Bh`#m#;C$3H_;J;jQLYZ~s`dERO@D7=*j2bkP-Kq=q#% z4f~(E1AM}b?;7+?~$69jrmTMtFGm;n=Tk@y(-{hgp#1-5u-1^?MVRu zXni`T$vwJvQwoPsZ*<$XZeQc?01E2Y;C++6a@LcRTlT^3Kn`L`8oA)*j0VbxsJQOO z(e2Nl%L!o!Emdv+N&CrQ>P__{+-84?t*!zB7IFvFW?gFM~W@1 z0evc8Ma19yt0HMD%@13oE5akn!hXmm(D`kB^d52jE*@k zzT+G7F?ki%QFV+v2s9e2NpSajxoTduPy9j^r~KuV zwd+7&23AmYn#joiYOwl1i!{{R`%|(NRI4<5ez1okYGQHbV4I4ma`=3gcymQ6eM$BWgg*WT zW2q4)#|;FlQsLn!LvISxZiYyhcZSCg)u~Z5R2tHFj}vh=#S?S>;SVNwRlim6?)Pdr zV$Q`tO~)A;EbpYp5=;;b*cl@>kYITG z^tAY}39#lE<0+tx@o}FCph~sf&_#PiZbnv6vBb8r;$IXoaO)-@pvT%Z27(6{zBv}X z##PD01=7F!0gvl=W^lDnaHzN`H-jpza8GIDC&f`L(lw6gQA#f5crbx~KQ?k$Emnig zk*6-Zvfpve3d)>@8+?^)9v2TAH}RD@C8xE+()6d_v!6N#nOZLb=wG&UeNN0Mdw!Y* z6WmLK&mk68Oz>P9F|*+>Lrhk8>)$NIzg0f)R4(H= zL1=i-dorvU7qEEb4%L7$7=XCAJ_J3jDh93ugW3UxY=DSdDL4etbJR8ZfVf8D1qrzl zRj3}Q7-N}>{}(Iv1WH^_vz}zfK`N);pS1jkoY2v4C{I%ThTij=POrJ(Nf~c=_9da% zHfQQJ*e1P^sm19yzy6T7+dnWnA1ln+$UPZSl99z-r;&K`VBN1>SEqX=j03a{!%zAT4_@KLpPCfw2c zJo6uAs7hw7eD%tY3k=#&d&KkQQwD|ay=BcdS*O@Ly0`H|qd9#45h^{w-c>INl99`g z<@tq4UXkF3+ANf_%n|+W{*^zhK#9rwX7vPbnJuVqGdNV)$N;`<&L!@zK|sDD-i}-& z$&Hah4IqxRsRS+mqGf>JsKD%N`Y)&95`b zzCRa7y=$-c$y-eN@NDwE;j95Sh@Fzr#)%4?=I@}u02C4Ic{^WD#v%64Zn`4i7`&{kBkPTnUNpIvUSAiWDqtFBXv1_D}^N+yXjD>@P zdLXR@)8gL^t)bzLhy4**4~iaK^FDW_!io07!r!FAi|_T(1Z?oh=Qo9XV$bryW_`{) zNG71c^w@7qRJ&$gotvgVSwLbx>#bDl?x$%%zic@%FZBSCor$bI8$QD=Os-1u3UIRi z`-USd)*MO*_;TgWQR)YW>@)Y`jjMGp&UNX;-RdQ@7xGA+q~_mj%gpMy{SPpwE=6qq z!YSXC+kq56bmKZT6D!E=vyB68ARypXaEM%EcI6F5YWgz{h$AAPpP50w{q zEll;%;7aIfjDhHMqkHl%0D3CYmPyG_m=!Ymy*0V9{0|%OFKdy%iJy`YNVD$Va_^zk zZ{~C4DjsmOBB7f!i=Vr@kKaV53VVdHKf?5^}>09LnV%e2W3+&zQ#g4 znj@RtKVyn#f5fgX8{9D-cWAM1&3V;!5Bc59xb$Qp*wy@a zhLZcP&Ac5E`3vVLj<}+;g;1tf39(}q*|G6*BGJiCF~8CQlcz_zDJ{SULGvl(sjmieg3{(iGzlk`gv=jY){_{_sqm+0#Nc9C<{oC(#o(do|rsJ zq3Oy|d7GbeCgtZ-2OjRf$2%MRzfed6`hnQ*vJk>U;gDAvifwrfi=91ZdGgX+T!vUn z4bbsa)z%pZhX%!dkux%eS&4M?-S}V`s-MOla=e)3Sc{wNpvcN6AnzVBZ>I~^`eC=fVY0O6dmq( z=k#%C>;HnOT44>ZI-gsQ7Uo3KLR!dZLATmW>oYWSYL8L{8$_Gy&=%n(yBJ#ObxaNMo`@4-O!v_HJU0tJ5csTBGve65OvkT-RCN@;lMHGN8C05u3aenah zC$aRhlc)W#@(6d-cwL^`rjNB9eLByXvZ#q)Fq$IbA|)OM?3rrZk7Tf4ebxt`v_R;B z{kg5u5kJ#v99lwa)b+HqmYXdWz9_8h_~wQi@EU;vpj#Di2V&PndUd3B8J$;4P0*U* zO|qMOgaxt?h~7A_hA`M^EicoE*+ftmpbK{tR6wns`)-MV``Q$;I?@sWQuvN0fGn(j zH{xb&KP+3J?DEdY&N}YACmwI} zN#!r>ZI~xRVa-X|6;naI|1I8cV;yO}oG(^#@CQh`WqhPtx9ifi6jjslg0=V_1dA5_ zZP(pt#H;nS#y~7F`DnoGd8f-<+&Tcvjan7^9l$oGKWSFw^cAXa^jw_p1t%=$F0gG+ zehal!O1^S*+)~8~8o>u+4!{<>g9}3TD?Z1@Fz9Mu^1bDp19aovN}i} znJ8hiF{4D0O=*d02JBshpntM`8IYU#ByE|c*~wMF7NG#xG{@$$h-dWtqbHhXggAa` za)!^mPNWRC^x87R$q)8jdf$b}JH3updaf)(-d&;gbgJ(dx-{HhX0B_~2|JmUOAW}52 zbXypv!dQEa^ZqoO`<9u;ISvE_v3Y3_9kp$t*Dt{$M}T{PBMIc)Fa^q~l494vHvX$p zpmv_9t#5gT)GLSA#4TVUjfQ z{^nsP+2&2Q4p=38l@*D9mv%XjnQ4Yb{mxhro2?%Du7tHyM3Lp~5ngM3I5gm?#kW|_ zK$t8ts7d<1pErJ9y}jU#<8pq5P*08g*YO-z1!tw3wm6p2%E8v_>Is%(D+%s&mUWn3 zW%eqL`~_o}Y)pHQjQg2sFEyQnWwZFf)`1UwlXV08oTqM1F*oGoCw!mztXmlFgNAI0dk8fZF`=K=-{*Dnf#lJ84PKVex6bU zbsy#VH+HaJh30MFb5DL;W~V8y?c=?newuE;8_I6N_*f|y=hOl=MUDaMhIS_-zZ*>n^*}gduMyTvr-T7VhmYrkg14>Y8R4#Qx$Feh{w95kq_y4MB{Oc40NN z`aBkFH_@(UJjq~-rYIt z%?{L;Z}?6^P8t<2W47=}s!sWJevmYqI9aXc&o!Z{xJr^|O5BXV0>8n=3L;~1{wr4g z)2x~YzA+G=2KH-I6icoP*^UY3gz=`77#tf`t@=(@TgNUZ3ntO;8OGj^YH#z%cTKm2 zNJ|ENWhz7$aQ$XV|MaLQEWY@KMo#&1*9=8EzL_4;KN^m^z_wXwniGU#Rencojknv(s0(v~ z1mV4~ldd#omzTCpo&iQ;Y#=w;iNAW`|9dp~%)jC4`V^o{5rC`3nSn+tH|VLKU8#z5 zkv45L*14-z!bqQ!*+b@z!VfK!6be}40C zzs-=m@yq&;7TZshXO7%834?}{-(U>M82P%sKcGvOX_}_5)9}otkH5zo!1s<&tuc!@ zSJ#r|K@wSQF}=p>N`0Nl6ML|xcMW%q(mIPOyc(=zmcZlB4vnNKOW8g;1*rwb<6< z0C9a>EHMP=`q8mZrcFY=S4A3+JZPln+y3gu|5qLUlbA-lf1_w$rTbMXhet$gii>mn z|GDi?Px$TfNhk4E-JGQKUE2lqft1mvuX$VZq0A5>F#@Esv)~hcnA`bV+BawI&)gipD^qPXE-YI<-9kYRQ{KMKSeoJ+0v&kgZ@9Z(`n@Z literal 0 HcmV?d00001 diff --git a/src/legacy/core_plugins/kibana/server/tutorials/aws_logs/index.js b/src/legacy/core_plugins/kibana/server/tutorials/aws_logs/index.js new file mode 100644 index 0000000000000..f85dd63449af8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/aws_logs/index.js @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/filebeat_instructions'; + +export function awsLogsSpecProvider(server, context) { + const moduleName = 'aws'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS']; + return { + id: 'awsLogs', + name: i18n.translate('kbn.server.tutorials.awsLogs.nameTitle', { + defaultMessage: 'AWS S3 based logs', + }), + category: TUTORIAL_CATEGORY.LOGGING, + shortDescription: i18n.translate('kbn.server.tutorials.awsLogs.shortDescription', { + defaultMessage: 'Collect AWS logs from S3 bucket with Filebeat.', + }), + longDescription: i18n.translate('kbn.server.tutorials.awsLogs.longDescription', { + defaultMessage: 'Collect AWS logs by exporting them to an S3 bucket which is configured with SQS notification. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-aws.html', + }, + }), + euiIconType: 'logoAWS', + artifacts: { + dashboards: [ + { + id: '4746e000-bacd-11e9-9f70-1f7bda85a5eb', + linkLabel: i18n.translate('kbn.server.tutorials.awsLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'AWS S3 server access log dashboard', + }), + isOverview: true + } + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-aws.html' + } + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/kibana/home/tutorial_resources/aws_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/cloudwatch_logs/index.js b/src/legacy/core_plugins/kibana/server/tutorials/cloudwatch_logs/index.js index 7617f313b2436..34bb168d5ab24 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/cloudwatch_logs/index.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/cloudwatch_logs/index.js @@ -25,11 +25,11 @@ export function cloudwatchLogsSpecProvider(context) { return { id: 'cloudwatchLogs', name: i18n.translate('kbn.server.tutorials.cloudwatchLogs.nameTitle', { - defaultMessage: 'Cloudwatch Logs', + defaultMessage: 'AWS Cloudwatch logs', }), category: TUTORIAL_CATEGORY.LOGGING, shortDescription: i18n.translate('kbn.server.tutorials.cloudwatchLogs.shortDescription', { - defaultMessage: 'Collect Cloudwatch logs with Functionbeat', + defaultMessage: 'Collect Cloudwatch logs with Functionbeat.', }), longDescription: i18n.translate('kbn.server.tutorials.cloudwatchLogs.longDescription', { defaultMessage: 'Collect Cloudwatch logs by deploying Functionbeat to run as \ @@ -39,7 +39,7 @@ export function cloudwatchLogsSpecProvider(context) { learnMoreLink: '{config.docs.beats.functionbeat}/functionbeat-getting-started.html', }, }), - //euiIconType: 'functionbeatApp', + euiIconType: 'logoAWS', artifacts: { dashboards: [ // TODO diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index 155d730ec3ede..2d1aaa92b1e26 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -79,6 +79,7 @@ import { emsBoundariesSpecProvider } from './ems'; import { consulMetricsSpecProvider } from './consul_metrics'; import { cockroachdbMetricsSpecProvider } from './cockroachdb_metrics'; import { traefikMetricsSpecProvider } from './traefik_metrics'; +import { awsLogsSpecProvider } from './aws_logs'; export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(systemLogsSpecProvider); @@ -144,4 +145,5 @@ export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(consulMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(cockroachdbMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(traefikMetricsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(awsLogsSpecProvider); } From 8acd526bb78382096746a7dd6acbc75fedc5dd74 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Fri, 22 Nov 2019 09:11:23 -0500 Subject: [PATCH 024/128] [Lens] Allow numeric terms aggs (#50177) --- .../indexpattern_suggestions.ts | 6 +++++- .../operations/definitions/terms.test.tsx | 18 ++++++++++++++++++ .../operations/definitions/terms.tsx | 2 +- .../operations/operations.test.ts | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index d2cf6261835fd..35e99fc4fe98d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -116,7 +116,11 @@ export function getDatasourceSuggestionsForField( } function getBucketOperation(field: IndexPatternField) { - return getOperationTypesForField(field).find(op => op === 'date_histogram' || op === 'terms'); + // We allow numeric bucket types in some cases, but it's generally not the right suggestion, + // so we eliminate it here. + if (field.type !== 'number') { + return getOperationTypesForField(field).find(op => op === 'date_histogram' || op === 'terms'); + } } function getExistingLayerSuggestionsForField( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx index e57745c11fc69..7b21ef92ab82b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx @@ -135,6 +135,24 @@ describe('terms', () => { scale: 'ordinal', }); + expect( + termsOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + type: 'number', + aggregationRestrictions: { + terms: { + agg: 'terms', + }, + }, + }) + ).toEqual({ + dataType: 'number', + isBucketed: true, + scale: 'ordinal', + }); + expect( termsOperation.getPossibleOperationForField({ aggregatable: true, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx index cb8893dd2f6dd..cd0dcc0b7e9ce 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx @@ -37,7 +37,7 @@ function isSortableByColumn(column: IndexPatternColumn) { } const DEFAULT_SIZE = 3; -const supportedTypes = new Set(['string', 'boolean', 'ip']); +const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']); export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { operationType: 'terms'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts index 0161b93effc52..3602491c6eb2c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts @@ -228,6 +228,20 @@ describe('getOperationTypesForField', () => { it('should list out all field-operation tuples for different operation meta data', () => { expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(` Array [ + Object { + "operationMetaData": Object { + "dataType": "number", + "isBucketed": true, + "scale": "ordinal", + }, + "operations": Array [ + Object { + "field": "bytes", + "operationType": "terms", + "type": "field", + }, + ], + }, Object { "operationMetaData": Object { "dataType": "string", From af19e9dd80bff6500ad5d7138aab283c35064ffa Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Fri, 22 Nov 2019 09:12:21 -0500 Subject: [PATCH 025/128] [ML] DF Analytics Outlier detection results - add search bar (#51235) * add search bar to outlierDetection results table * show empty results error message in table so user can retry query * remove unused translation * type updates after branch update --- .../data_frame_analytics/common/analytics.ts | 10 ++ .../data_frame_analytics/common/index.ts | 2 + .../components/exploration/exploration.tsx | 160 +++++++++++------- .../exploration/use_explore_data.ts | 33 ++-- .../regression_exploration/evaluate_panel.tsx | 2 +- .../regression_exploration.tsx | 3 +- .../regression_exploration/results_table.tsx | 3 +- .../use_explore_data.ts | 12 +- .../translations/translations/zh-CN.json | 1 - 9 files changed, 138 insertions(+), 88 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts index b1eedc1378d43..f910b8ea8a233 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts @@ -33,6 +33,16 @@ interface RegressionAnalysis { export const SEARCH_SIZE = 1000; +export const defaultSearchQuery = { + match_all: {}, +}; + +export interface SearchQuery { + track_total_hits?: boolean; + query: SavedSearchQuery; + sort?: any; +} + export enum INDEX_STATUS { UNUSED, LOADING, diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts index 112f828f9897e..02a1c30259cce 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts @@ -24,6 +24,8 @@ export { getPredictedFieldName, INDEX_STATUS, SEARCH_SIZE, + defaultSearchQuery, + SearchQuery, } from './analytics'; export { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index 9cc469f83a534..fea4c861551a3 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useState } from 'react'; +import React, { FC, Fragment, useEffect, useState } from 'react'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; @@ -18,13 +18,16 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, + EuiFormRow, EuiPanel, EuiPopover, EuiPopoverTitle, EuiProgress, + EuiSpacer, EuiText, EuiTitle, EuiToolTip, + Query, } from '@elastic/eui'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; @@ -51,12 +54,18 @@ import { EsDoc, MAX_COLUMNS, INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, } from '../../../../common'; import { getOutlierScoreFieldName } from './common'; import { useExploreData } from './use_explore_data'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { + DATA_FRAME_TASK_STATE, + Query as QueryType, +} from '../../../analytics_management/components/analytics_list/common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; const customColorScaleFactory = (n: number) => (t: number) => { if (t < 1 / n) { @@ -99,6 +108,10 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(25); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [searchError, setSearchError] = useState(undefined); + const [searchString, setSearchString] = useState(undefined); + useEffect(() => { (async function() { const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics( @@ -119,23 +132,9 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { ? euiThemeDark : euiThemeLight; - const [clearTable, setClearTable] = useState(false); - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - // EuiInMemoryTable has an issue with dynamic sortable columns - // and will trigger a full page Kibana error in such a case. - // The following is a workaround until this is solved upstream: - // - If the sortable/columns config changes, - // the table will be unmounted/not rendered. - // This is what setClearTable(true) in toggleColumn() does. - // - After that on next render it gets re-enabled. To make sure React - // doesn't consolidate the state updates, setTimeout is used. - if (clearTable) { - setTimeout(() => setClearTable(false), 0); - } - function toggleColumnsPopover() { setColumnsPopoverVisible(!isColumnsPopoverVisible); } @@ -146,7 +145,6 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { function toggleColumn(column: EsFieldName) { if (tableItems.length > 0 && jobConfig !== undefined) { - setClearTable(true); // spread to a new array otherwise the component wouldn't re-render setSelectedFields([...toggleSelectedField(selectedFields, column)]); } @@ -309,6 +307,17 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { ); } + useEffect(() => { + if (jobConfig !== undefined) { + const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); + const outlierScoreFieldSelected = selectedFields.includes(outlierScoreFieldName); + + const field = outlierScoreFieldSelected ? outlierScoreFieldName : selectedFields[0]; + const direction = outlierScoreFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [JSON.stringify(searchQuery)]); + useEffect(() => { // by default set the sorting to descending on the `outlier_score` field. // if that's not available sort ascending on the first column. @@ -319,7 +328,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const field = outlierScoreFieldSelected ? outlierScoreFieldName : selectedFields[0]; const direction = outlierScoreFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field, direction }); + loadExploreData({ field, direction, searchQuery }); return; } }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); @@ -344,8 +353,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { setPageSize(size); if (sort.field !== sortField || sort.direction !== sortDirection) { - setClearTable(true); - loadExploreData(sort); + loadExploreData({ ...sort, searchQuery }); } }; } @@ -358,11 +366,37 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { hidePerPageOptions: false, }; + const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { + if (error) { + setSearchError(error.message); + } else { + try { + const esQueryDsl = Query.toESQuery(query); + setSearchQuery(esQueryDsl); + setSearchString(query.text); + setSearchError(undefined); + } catch (e) { + setSearchError(e.toString()); + } + } + }; + + const search = { + onChange: onQueryChange, + defaultQuery: searchString, + box: { + incremental: false, + placeholder: i18n.translate('xpack.ml.dataframe.analytics.exploration.searchBoxPlaceholder', { + defaultMessage: 'E.g. avg>0.5', + }), + }, + }; + if (jobConfig === undefined) { return null; } - - if (status === INDEX_STATUS.ERROR) { + // if it's a searchBar syntax error leave the table visible so they can try again + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { return ( @@ -379,32 +413,16 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { ); } - if (status === INDEX_STATUS.LOADED && tableItems.length === 0) { - return ( - - - - - - - {getTaskStateBadge(jobStatus)} - - - -

    - {i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { - defaultMessage: - 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', - })} -

    - - - ); + let tableError = + status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') + ? errorMessage + : searchError; + + if (status === INDEX_STATUS.LOADED && tableItems.length === 0 && tableError === undefined) { + tableError = i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { + defaultMessage: + 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', + }); } return ( @@ -483,20 +501,38 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { {status !== INDEX_STATUS.LOADING && ( )} - {clearTable === false && columns.length > 0 && sortField !== '' && ( - + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && sortField !== '' && ( + + {tableItems.length === SEARCH_SIZE && ( + + + + )} + + + )} ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts index 2a07bc1251a31..a0728e0bae446 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts @@ -20,16 +20,21 @@ import { EsFieldName, INDEX_STATUS, SEARCH_SIZE, + defaultSearchQuery, + SearchQuery, } from '../../../../common'; import { getOutlierScoreFieldName } from './common'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; type TableItem = Record; interface LoadExploreDataArg { field: string; direction: SortDirection; + searchQuery: SavedSearchQuery; } + export interface UseExploreDataReturnType { errorMessage: string; loadExploreData: (arg: LoadExploreDataArg) => void; @@ -50,7 +55,7 @@ export const useExploreData = ( const [sortField, setSortField] = useState(''); const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); - const loadExploreData = async ({ field, direction }: LoadExploreDataArg) => { + const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => { if (jobConfig !== undefined) { setErrorMessage(''); setStatus(INDEX_STATUS.LOADING); @@ -58,19 +63,24 @@ export const useExploreData = ( try { const resultsField = jobConfig.dest.results_field; + const body: SearchQuery = { + query: searchQuery, + }; + + if (field !== undefined) { + body.sort = [ + { + [field]: { + order: direction, + }, + }, + ]; + } + const resp: SearchResponse = await ml.esSearch({ index: jobConfig.dest.index, size: SEARCH_SIZE, - body: { - query: { match_all: {} }, - sort: [ - { - [field]: { - order: direction, - }, - }, - ], - }, + body, }); setSortField(field); @@ -135,6 +145,7 @@ export const useExploreData = ( loadExploreData({ field: getOutlierScoreFieldName(jobConfig), direction: SORT_DIRECTION.DESC, + searchQuery: defaultSearchQuery, }); } }, [jobConfig && jobConfig.id]); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 8bb44da74087c..d877ed40e587d 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -25,8 +25,8 @@ import { getEvalQueryBody, isRegressionResultsSearchBoolQuery, RegressionResultsSearchQuery, + SearchQuery, } from '../../../../common/analytics'; -import { SearchQuery } from './use_explore_data'; interface Props { jobConfig: DataFrameAnalyticsConfig; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 8ebbd8c401948..2f7ff4feed2a8 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -12,8 +12,7 @@ import { DataFrameAnalyticsConfig } from '../../../../common'; import { EvaluatePanel } from './evaluate_panel'; import { ResultsTable } from './results_table'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { defaultSearchQuery } from './use_explore_data'; -import { RegressionResultsSearchQuery } from '../../../../common/analytics'; +import { RegressionResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; interface GetDataFrameAnalyticsResponse { count: number; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index a1d4261d2cf32..ec504492e0a5e 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -50,11 +50,12 @@ import { getPredictedFieldName, INDEX_STATUS, SEARCH_SIZE, + defaultSearchQuery, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData, defaultSearchQuery } from './use_explore_data'; +import { useExploreData } from './use_explore_data'; import { ExplorationTitle } from './regression_exploration'; const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts index 332451c6e4d7a..bf3565abd8de4 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts @@ -27,12 +27,10 @@ import { getPredictedFieldName, INDEX_STATUS, SEARCH_SIZE, + defaultSearchQuery, + SearchQuery, } from '../../../../common'; -export const defaultSearchQuery = { - match_all: {}, -}; - type TableItem = Record; interface LoadExploreDataArg { @@ -49,12 +47,6 @@ export interface UseExploreDataReturnType { tableItems: TableItem[]; } -export interface SearchQuery { - track_total_hits?: boolean; - query: SavedSearchQuery; - sort?: any; -} - export const useExploreData = ( jobConfig: DataFrameAnalyticsConfig | undefined, selectedFields: EsFieldName[], diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 582fafe8e782d..2925c777f84d2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7749,7 +7749,6 @@ "xpack.ml.dataframe.analytics.exploration.indexObjectToolTipContent": "无法显示此基于对象的列的完整内容。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "作业 ID {jobId}", "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "该索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutTitle": "空的索引查询结果。", "xpack.ml.dataframe.analytics.exploration.selectColumnsAriaLabel": "选择列", "xpack.ml.dataframe.analytics.exploration.selectFieldsPopoverTitle": "选择字段", "xpack.ml.dataframe.analytics.exploration.title": "分析浏览", From e09cde9842a5d7fbf4574d4e9939636e634bd508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= Date: Fri, 22 Nov 2019 15:26:43 +0100 Subject: [PATCH 026/128] [APM] Add missing semi-colon to styled component (#51436) --- .../WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index cd892b370219a..1a3f1f6831ff3 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -33,7 +33,7 @@ registerLanguage('sql', sql); const DatabaseStatement = styled.div` padding: ${px(units.half)} ${px(unit)}; - background: ${tint(0.1, theme.euiColorWarning)} + background: ${tint(0.1, theme.euiColorWarning)}; border-radius: ${borderRadius}; border: 1px solid ${theme.euiColorLightShade}; font-family: ${fontFamilyCode}; From 8b2835a986f18a23b37b5b980239634a3d548fd3 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 22 Nov 2019 09:28:42 -0700 Subject: [PATCH 027/128] disable babel/register cache in CI (#51300) * disable babel/register cache in CI * log stdio when logs don't match expectation * upgrade babel/register so that cache files are not touched --- package.json | 2 +- src/dev/ci_setup/setup_env.sh | 9 +++ .../config/__tests__/deprecation_warnings.js | 9 ++- x-pack/package.json | 2 +- yarn.lock | 63 ++++++------------- 5 files changed, 37 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 34086c6ba684d..04415b481d5dd 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ }, "dependencies": { "@babel/core": "^7.5.5", - "@babel/register": "^7.5.5", + "@babel/register": "^7.7.0", "@elastic/charts": "^14.0.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 805b77365e624..6cfcaca5843b3 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -22,6 +22,15 @@ C_RESET='\033[0m' # Reset color ### export FORCE_COLOR=1 +### +### The @babel/register cache collects the build output from each file in +### a map, in memory, and then when the process exits it writes that to the +### babel cache file as a JSON encoded object. Stringifying that object +### causes OOMs on CI regularly enough that we need to find another solution, +### and until we do we need to disable the cache +### +export BABEL_DISABLE_CACHE=true + ### ### check that we seem to be in a kibana project ### diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js index db5c0e9665492..7cf6fdcd85ad5 100644 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ b/src/legacy/server/config/__tests__/deprecation_warnings.js @@ -38,6 +38,7 @@ describe('config/deprecation warnings', function () { ], { stdio: ['ignore', 'pipe', 'pipe'], env: { + ...process.env, CREATE_SERVER_OPTS: JSON.stringify({ logging: { quiet: false, @@ -105,7 +106,11 @@ describe('config/deprecation warnings', function () { line.tags.includes('warning') ); - expect(deprecationLines).to.have.length(1); - expect(deprecationLines[0]).to.have.property('message', 'uiSettings.enabled is deprecated and is no longer used'); + try { + expect(deprecationLines).to.have.length(1); + expect(deprecationLines[0]).to.have.property('message', 'uiSettings.enabled is deprecated and is no longer used'); + } catch (error) { + throw new Error(`Expected stdio to include deprecation message about uiSettings.enabled\n\nstdio:\n${stdio}\n\n`); + } }); }); diff --git a/x-pack/package.json b/x-pack/package.json index 927efbffa132e..84ce92bf8e9e6 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -175,7 +175,7 @@ }, "dependencies": { "@babel/core": "^7.5.5", - "@babel/register": "^7.5.5", + "@babel/register": "^7.7.0", "@babel/runtime": "^7.5.5", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", diff --git a/yarn.lock b/yarn.lock index 7f33c00c5460a..68b9a74829281 100644 --- a/yarn.lock +++ b/yarn.lock @@ -963,17 +963,16 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-typescript" "^7.3.2" -"@babel/register@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.5.5.tgz#40fe0d474c8c8587b28d6ae18a03eddad3dac3c1" - integrity sha512-pdd5nNR+g2qDkXZlW1yRCWFlNrAn2PPdnZUB72zjX4l1Vv4fMRRLwyf+n/idFCLI1UgVGboUU8oVziwTBiyNKQ== +"@babel/register@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.7.0.tgz#4e23ecf840296ef79c605baaa5c89e1a2426314b" + integrity sha512-HV3GJzTvSoyOMWGYn2TAh6uL6g+gqKTgEZ99Q3+X9UURT1VPT/WcU46R61XftIc5rXytcOHZ4Z0doDlsjPomIg== dependencies: - core-js "^3.0.0" find-cache-dir "^2.0.0" lodash "^4.17.13" - mkdirp "^0.5.1" + make-dir "^2.1.0" pirates "^4.0.0" - source-map-support "^0.5.9" + source-map-support "^0.5.16" "@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.4.2": version "7.5.5" @@ -8633,11 +8632,6 @@ core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.3.tgz#95700bca5f248f5f78c0ec63e784eca663ec4138" - integrity sha512-PWZ+ZfuaKf178BIAg+CRsljwjIMRV8MY00CbZczkR6Zk5LfkSkjGoaab3+bqRQWVITNZxQB7TFYz+CFcyuamvA== - core-js@^3.0.1, core-js@^3.0.4, core-js@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" @@ -25592,34 +25586,10 @@ source-map-support@^0.3.2: dependencies: source-map "0.1.32" -source-map-support@^0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13" - integrity sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@^0.5.6: - version "0.5.9" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" - integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@^0.5.9: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@~0.5.12: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== +source-map-support@^0.5.1, source-map-support@^0.5.16, source-map-support@^0.5.6, source-map-support@^0.5.9, source-map-support@~0.5.12: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -25636,10 +25606,10 @@ source-map@0.1.32: dependencies: amdefine ">=0.0.4" -"source-map@>= 0.1.2", source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +"source-map@>= 0.1.2": + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== source-map@^0.4.2: version "0.4.4" @@ -25653,6 +25623,11 @@ source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, sour resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + source-map@~0.1.30, source-map@~0.1.33: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" From b2e200437ea160fd63f2e2974097a99545527a3a Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 22 Nov 2019 09:53:25 -0700 Subject: [PATCH 028/128] =?UTF-8?q?[master]=20Update=20where=20log=20files?= =?UTF-8?q?=20are=20written=20using=20systemd=20(#47=E2=80=A6=20(#51463)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At least for kibana-oss 7.4, this is how I can access Kibana logs. The file `/var/log/kibana` is not created and if I set it as a log file, kibana does not have permission to write there. See also: https://github.com/elastic/kibana/issues/6579 --- docs/setup/install/systemd.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/setup/install/systemd.asciidoc b/docs/setup/install/systemd.asciidoc index 07d995244d511..3053972f9e384 100644 --- a/docs/setup/install/systemd.asciidoc +++ b/docs/setup/install/systemd.asciidoc @@ -18,5 +18,5 @@ sudo systemctl stop kibana.service -------------------------------------------- These commands provide no feedback as to whether Kibana was started -successfully or not. Instead, this information will be written in the log -files located in `/var/log/kibana/`. +successfully or not. Log information can be accessed via +`journalctl -u kibana.service`. From d5b0fc82f23d4f27d588d763f37f6b6b39c7c441 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Fri, 22 Nov 2019 16:55:43 +0000 Subject: [PATCH 029/128] [SIEM] Add DNS histogram (#50409) * add histogram * fix types error * rename matrix histogram component * clean up * add dns histogram container * wrap passed in function with useCallback * isolate utils functions * extract types * disable chart legend for dns histogram * add dns bytes in --- .../index.test.tsx | 8 +- .../index.tsx | 96 ++++------------ .../components/matrix_histogram/types.ts | 32 ++++++ .../components/matrix_histogram/utils.ts | 93 ++++++++++++++++ .../hosts/authentications_over_time/index.tsx | 18 +-- .../hosts/authentications_over_time/utils.ts | 50 ++++----- .../page/hosts/events_over_time/index.tsx | 10 +- .../page/network/dns_histogram/index.tsx | 32 ++++++ .../page/network/dns_histogram/translation.ts | 11 ++ .../page/network/network_dns_table/mock.ts | 52 +++++++++ .../containers/network_dns/index.gql_query.ts | 5 + .../public/containers/network_dns/index.tsx | 29 ++++- .../siem/public/graphql/introspection.json | 67 ++++++++++++ .../plugins/siem/public/graphql/types.ts | 22 ++++ .../network/navigation/dns_query_tab_body.tsx | 103 ++++++++++++------ .../network/navigation/network_routes.tsx | 10 +- .../public/pages/network/navigation/types.ts | 3 + .../siem/server/graphql/network/schema.gql.ts | 7 ++ .../plugins/siem/server/graphql/types.ts | 44 ++++++++ .../lib/network/elasticsearch_adapter.ts | 29 +++++ x-pack/test/tsconfig.json | 2 +- 21 files changed, 566 insertions(+), 157 deletions(-) rename x-pack/legacy/plugins/siem/public/components/{matrix_over_time => matrix_histogram}/index.test.tsx (88%) rename x-pack/legacy/plugins/siem/public/components/{matrix_over_time => matrix_histogram}/index.tsx (53%) create mode 100644 x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/translation.ts diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx similarity index 88% rename from x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.test.tsx rename to x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx index 9d2ef203361bf..bdd8a0c544ed8 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import { MatrixOverTimeHistogram } from '.'; +import { MatrixHistogram } from '.'; jest.mock('@elastic/eui', () => { return { @@ -53,7 +53,7 @@ describe('Load More Events Table Component', () => { }; describe('rendering', () => { test('it renders EuiLoadingContent on initialLoad', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(`[data-test-subj="initialLoadingPanelMatrixOverTime"]`)).toBeTruthy(); }); @@ -65,7 +65,7 @@ describe('Load More Events Table Component', () => { totalCount: 10, loading: true, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('.loader')).toBeTruthy(); }); @@ -76,7 +76,7 @@ describe('Load More Events Table Component', () => { totalCount: 10, loading: false, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(`.barchart`)).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx similarity index 53% rename from x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx rename to x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx index 75e1531ea2b5b..f79c61a29c26b 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_over_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -5,100 +5,50 @@ */ import React, { useState, useEffect } from 'react'; -import { ScaleType, niceTimeFormatter, Position } from '@elastic/charts'; +import { ScaleType } from '@elastic/charts'; -import { getOr, head, last } from 'lodash/fp'; import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { EuiLoadingContent } from '@elastic/eui'; import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; -import { ChartSeriesData, UpdateDateRange } from '../charts/common'; -import { MatrixOverTimeHistogramData } from '../../graphql/types'; +import { ChartSeriesData } from '../charts/common'; import { DEFAULT_DARK_MODE } from '../../../common/constants'; import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; import { Loader } from '../loader'; import { Panel } from '../panel'; +import { getBarchartConfigs, getCustomChartData } from './utils'; +import { MatrixHistogramProps, MatrixHistogramDataTypes } from './types'; -export interface MatrixOverTimeBasicProps { - id: string; - data: MatrixOverTimeHistogramData[]; - loading: boolean; - startDate: number; - endDate: number; - updateDateRange: UpdateDateRange; - totalCount: number; -} - -export interface MatrixOverTimeProps extends MatrixOverTimeBasicProps { - customChartData?: ChartSeriesData[]; - title: string; - subtitle?: string; - dataKey: string; -} - -const getBarchartConfigs = (from: number, to: number, onBrushEnd: UpdateDateRange) => ({ - series: { - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - stackAccessors: ['g'], - }, - axis: { - xTickFormatter: niceTimeFormatter([from, to]), - yTickFormatter: (value: string | number): string => value.toLocaleString(), - tickSize: 8, - }, - settings: { - legendPosition: Position.Bottom, - onBrushEnd, - showLegend: true, - theme: { - scales: { - barsPadding: 0.08, - }, - chartMargins: { - left: 0, - right: 0, - top: 0, - bottom: 0, - }, - chartPaddings: { - left: 0, - right: 0, - top: 0, - bottom: 0, - }, - }, - }, - customHeight: 324, -}); - -export const MatrixOverTimeHistogram = ({ - customChartData, - id, - loading, +export const MatrixHistogram = ({ data, dataKey, endDate, - updateDateRange, + id, + loading, + mapping, + scaleType = ScaleType.Time, startDate, - title, subtitle, + title, totalCount, -}: MatrixOverTimeProps) => { - const bucketStartDate = getOr(startDate, 'x', head(data)); - const bucketEndDate = getOr(endDate, 'x', last(data)); - const barchartConfigs = getBarchartConfigs(bucketStartDate!, bucketEndDate!, updateDateRange); + updateDateRange, + yTickFormatter, + showLegend, +}: MatrixHistogramProps) => { + const barchartConfigs = getBarchartConfigs({ + from: startDate, + to: endDate, + onBrushEnd: updateDateRange, + scaleType, + yTickFormatter, + showLegend, + }); const [showInspect, setShowInspect] = useState(false); const [darkMode] = useKibanaUiSetting(DEFAULT_DARK_MODE); const [loadingInitial, setLoadingInitial] = useState(false); - const barChartData: ChartSeriesData[] = customChartData || [ - { - key: dataKey, - value: data, - }, - ]; + const barChartData: ChartSeriesData[] = getCustomChartData(data, mapping); useEffect(() => { if (totalCount >= 0 && loadingInitial) { diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts new file mode 100644 index 0000000000000..edcd8e3cb9d5c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ScaleType } from '@elastic/charts'; +import { MatrixOverTimeHistogramData, MatrixOverOrdinalHistogramData } from '../../graphql/types'; +import { AuthMatrixDataFields } from '../page/hosts/authentications_over_time/utils'; +import { UpdateDateRange } from '../charts/common'; + +export type MatrixHistogramDataTypes = MatrixOverTimeHistogramData | MatrixOverOrdinalHistogramData; +export type MatrixHistogramMappingTypes = AuthMatrixDataFields; +export interface MatrixHistogramBasicProps { + data: T[]; + endDate: number; + id: string; + loading: boolean; + mapping?: MatrixHistogramMappingTypes; + startDate: number; + totalCount: number; + updateDateRange: UpdateDateRange; +} + +export interface MatrixHistogramProps extends MatrixHistogramBasicProps { + dataKey?: string; + scaleType?: ScaleType; + subtitle?: string; + title?: string; + yTickFormatter?: (value: number) => string; + showLegend?: boolean; +} diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts new file mode 100644 index 0000000000000..1eb5e96b86857 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ScaleType, niceTimeFormatter, Position } from '@elastic/charts'; +import { get, groupBy, map, toPairs } from 'lodash/fp'; +import numeral from '@elastic/numeral'; +import { UpdateDateRange, ChartSeriesData } from '../charts/common'; +import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types'; + +export const getBarchartConfigs = ({ + from, + to, + scaleType, + onBrushEnd, + yTickFormatter, + showLegend, +}: { + from: number; + to: number; + scaleType: ScaleType; + onBrushEnd: UpdateDateRange; + yTickFormatter?: (value: number) => string; + showLegend?: boolean; +}) => ({ + series: { + xScaleType: scaleType || ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: ['g'], + }, + axis: { + xTickFormatter: scaleType === ScaleType.Time ? niceTimeFormatter([from, to]) : undefined, + yTickFormatter: + yTickFormatter != null + ? yTickFormatter + : (value: string | number): string => value.toLocaleString(), + tickSize: 8, + }, + settings: { + legendPosition: Position.Bottom, + onBrushEnd, + showLegend: showLegend || true, + theme: { + scales: { + barsPadding: 0.08, + }, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + chartPaddings: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + }, + }, + customHeight: 324, +}); + +export const formatToChartDataItem = ([key, value]: [ + string, + MatrixHistogramDataTypes[] +]): ChartSeriesData => ({ + key, + value, +}); + +export const getCustomChartData = ( + data: MatrixHistogramDataTypes[], + mapping?: MatrixHistogramMappingTypes +): ChartSeriesData[] => { + const dataGroupedByEvent = groupBy('g', data); + const dataGroupedEntries = toPairs(dataGroupedByEvent); + const formattedChartData = map(formatToChartDataItem, dataGroupedEntries); + + if (mapping) + return map((item: ChartSeriesData) => { + const customColor = get(`${item.key}.color`, mapping); + item.color = customColor; + return item; + }, formattedChartData); + else return formattedChartData; +}; + +export const bytesFormatter = (value: number) => { + return numeral(value).format('0,0.[0]b'); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/index.tsx index ad343933a268c..f9e63ee60da5b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/index.tsx @@ -7,21 +7,23 @@ import React from 'react'; import * as i18n from './translation'; -import { getCustomChartData } from './utils'; -import { MatrixOverTimeHistogram, MatrixOverTimeBasicProps } from '../../../matrix_over_time'; +import { MatrixHistogram } from '../../../matrix_histogram'; +import { MatrixHistogramBasicProps } from '../../../matrix_histogram/types'; +import { MatrixOverTimeHistogramData } from '../../../../graphql/types'; +import { authMatrixDataMappingFields } from './utils'; -export const AuthenticationsOverTimeHistogram = (props: MatrixOverTimeBasicProps) => { +export const AuthenticationsOverTimeHistogram = ( + props: MatrixHistogramBasicProps +) => { const dataKey = 'authenticationsOverTime'; const { data, ...matrixOverTimeProps } = props; - const customChartData = getCustomChartData(data); - return ( - ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/utils.ts b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/utils.ts index 3cc89eeff6540..e0e2d21b40446 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_over_time/utils.ts @@ -4,36 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { groupBy, map, toPairs } from 'lodash/fp'; - import { ChartSeriesData } from '../../../charts/common'; -import { MatrixOverTimeHistogramData } from '../../../../graphql/types'; import { KpiHostsChartColors } from '../kpi_hosts/types'; -const formatToChartDataItem = ([key, value]: [ - string, - MatrixOverTimeHistogramData[] -]): ChartSeriesData => ({ - key, - value, -}); - -const addCustomColors = (item: ChartSeriesData) => { - if (item.key === 'authentication_success') { - item.color = KpiHostsChartColors.authSuccess; - } - - if (item.key === 'authentication_failure') { - item.color = KpiHostsChartColors.authFailure; - } - - return item; -}; - -export const getCustomChartData = (data: MatrixOverTimeHistogramData[]): ChartSeriesData[] => { - const dataGroupedByEvent = groupBy('g', data); - const dataGroupedEntries = toPairs(dataGroupedByEvent); - const formattedChartData = map(formatToChartDataItem, dataGroupedEntries); - - return map(addCustomColors, formattedChartData); +enum AuthMatrixDataGroup { + authSuccess = 'authentication_success', + authFailure = 'authentication_failure', +} + +export interface AuthMatrixDataFields { + [AuthMatrixDataGroup.authSuccess]: ChartSeriesData; + [AuthMatrixDataGroup.authFailure]: ChartSeriesData; +} + +export const authMatrixDataMappingFields: AuthMatrixDataFields = { + [AuthMatrixDataGroup.authSuccess]: { + key: AuthMatrixDataGroup.authSuccess, + value: null, + color: KpiHostsChartColors.authSuccess, + }, + [AuthMatrixDataGroup.authFailure]: { + key: AuthMatrixDataGroup.authFailure, + value: null, + color: KpiHostsChartColors.authFailure, + }, }; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/index.tsx index 8b41619199653..8273ecffdf9b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/index.tsx @@ -7,16 +7,20 @@ import React from 'react'; import * as i18n from './translation'; -import { MatrixOverTimeHistogram, MatrixOverTimeBasicProps } from '../../../matrix_over_time'; +import { MatrixHistogram } from '../../../matrix_histogram'; +import { MatrixHistogramBasicProps } from '../../../matrix_histogram/types'; +import { MatrixOverTimeHistogramData } from '../../../../graphql/types'; -export const EventsOverTimeHistogram = (props: MatrixOverTimeBasicProps) => { +export const EventsOverTimeHistogram = ( + props: MatrixHistogramBasicProps +) => { const dataKey = 'eventsOverTime'; const { totalCount } = props; const subtitle = `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(totalCount)}`; const { ...matrixOverTimeProps } = props; return ( - +) => { + const dataKey = 'histogram'; + const { ...matrixOverTimeProps } = props; + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/translation.ts b/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/translation.ts new file mode 100644 index 0000000000000..bb822651f10ce --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/network/dns_histogram/translation.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NETWORK_DNS_HISTOGRAM = i18n.translate('xpack.siem.DNS.histogramTitle', { + defaultMessage: 'Top DNS domains bytes count', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts index 29ea5f9d12588..281125edb9dc4 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts @@ -126,5 +126,57 @@ export const mockData: { NetworkDns: NetworkDnsData } = { fakeTotalCount: 50, showMorePagesIndicator: true, }, + histogram: [ + { + x: 'nflxvideo.net', + g: 'nflxvideo.net', + y: 12546, + }, + { + x: 'apple.com', + g: 'apple.com', + y: 31687, + }, + { + x: 'googlevideo.com', + g: 'googlevideo.com', + y: 16292, + }, + { + x: 'netflix.com', + g: 'netflix.com', + y: 218193, + }, + { + x: 'samsungcloudsolution.com', + g: 'samsungcloudsolution.com', + y: 11702, + }, + { + x: 'doubleclick.net', + g: 'doubleclick.net', + y: 14372, + }, + { + x: 'digitalocean.com', + g: 'digitalocean.com', + y: 4111, + }, + { + x: 'samsungelectronics.com', + g: 'samsungelectronics.com', + y: 36592, + }, + { + x: 'google.com', + g: 'google.com', + y: 8072, + }, + { + x: 'samsungcloudsolution.net', + g: 'samsungcloudsolution.net', + y: 11518, + }, + ], }, }; diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts index 365d93ee7e756..da83e09e4629a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts @@ -50,6 +50,11 @@ export const networkDnsQuery = gql` dsl response } + histogram { + x + y + g + } } } } diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx index af6806824d338..592fe43b9873f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx @@ -16,15 +16,17 @@ import { NetworkDnsEdges, NetworkDnsSortField, PageInfoPaginated, + MatrixOverOrdinalHistogramData, } from '../../graphql/types'; import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; import { createFilter, getDefaultFetchPolicy } from '../helpers'; import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; import { networkDnsQuery } from './index.gql_query'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; const ID = 'networkDnsQuery'; - +const HISTOGRAM_ID = 'networkDnsHistogramQuery'; export interface NetworkDnsArgs { id: string; inspect: inputsModel.InspectQuery; @@ -35,6 +37,7 @@ export interface NetworkDnsArgs { pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; totalCount: number; + histogram: MatrixOverOrdinalHistogramData[]; } export interface OwnProps extends QueryTemplatePaginatedProps { @@ -52,7 +55,7 @@ export interface NetworkDnsComponentReduxProps { type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps; -class NetworkDnsComponentQuery extends QueryTemplatePaginated< +export class NetworkDnsComponentQuery extends QueryTemplatePaginated< NetworkDnsProps, GetNetworkDnsQuery.Query, GetNetworkDnsQuery.Variables @@ -129,6 +132,7 @@ class NetworkDnsComponentQuery extends QueryTemplatePaginated< pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), refetch: this.memoizedRefetchQuery(variables, limit, refetch), totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), + histogram: getOr(null, 'source.NetworkDns.histogram', data), }); }} @@ -144,6 +148,24 @@ const makeMapStateToProps = () => { return { ...getNetworkDnsSelector(state), isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +const makeMapHistogramStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + isInspected, + id, }; }; @@ -151,3 +173,6 @@ const makeMapStateToProps = () => { }; export const NetworkDnsQuery = connect(makeMapStateToProps)(NetworkDnsComponentQuery); +export const NetworkDnsHistogramQuery = connect(makeMapHistogramStateToProps)( + NetworkDnsComponentQuery +); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 9bde4bf47fff0..a93168c835293 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -8034,6 +8034,26 @@ "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "histogram", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MatrixOverOrdinalHistogramData", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -8135,6 +8155,53 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "MatrixOverOrdinalHistogramData", + "description": "", + "fields": [ + { + "name": "x", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "y", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "g", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "NetworkHttpSortField", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 833102a0d00bc..ad05e42bcd859 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -1626,6 +1626,8 @@ export interface NetworkDnsData { pageInfo: PageInfoPaginated; inspect?: Maybe; + + histogram?: Maybe; } export interface NetworkDnsEdges { @@ -1648,6 +1650,14 @@ export interface NetworkDnsItem { uniqueDomains?: Maybe; } +export interface MatrixOverOrdinalHistogramData { + x: string; + + y: number; + + g: string; +} + export interface NetworkHttpData { edges: NetworkHttpEdges[]; @@ -3311,6 +3321,8 @@ export namespace GetNetworkDnsQuery { pageInfo: PageInfo; inspect: Maybe; + + histogram: Maybe; }; export type Edges = { @@ -3360,6 +3372,16 @@ export namespace GetNetworkDnsQuery { response: string[]; }; + + export type Histogram = { + __typename?: 'MatrixOverOrdinalHistogramData'; + + x: string; + + y: number; + + g: string; + }; } export namespace GetNetworkHttpQuery { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx index 589c3b5f53533..34ff35bd145a2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx @@ -7,13 +7,16 @@ import React from 'react'; import { getOr } from 'lodash/fp'; +import { EuiSpacer } from '@elastic/eui'; import { NetworkDnsTable } from '../../../components/page/network/network_dns_table'; -import { NetworkDnsQuery } from '../../../containers/network_dns'; +import { NetworkDnsQuery, NetworkDnsHistogramQuery } from '../../../containers/network_dns'; import { manageQuery } from '../../../components/page/manage_query'; import { DnsQueryTabBodyProps } from './types'; +import { NetworkDnsHistogram } from '../../../components/page/network/dns_histogram'; const NetworkDnsTableManage = manageQuery(NetworkDnsTable); +const NetworkDnsHistogramManage = manageQuery(NetworkDnsHistogram); export const DnsQueryTabBody = ({ to, @@ -22,42 +25,70 @@ export const DnsQueryTabBody = ({ from, setQuery, type, + updateDateRange = () => {}, }: DnsQueryTabBodyProps) => ( - - {({ - totalCount, - loading, - networkDns, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - + <> + + {({ totalCount, loading, id, inspect, refetch, histogram }) => ( + + )} + + + + {({ + totalCount, + loading, + networkDns, + pageInfo, + loadPage, + id, + inspect, + isInspected, + refetch, + histogram, + }) => ( + + )} + + ); DnsQueryTabBody.displayName = 'DNSQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index 955670b4b098d..0f373be94b45b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -19,6 +19,7 @@ import { DnsQueryTabBody } from './dns_query_tab_body'; import { ConditionalFlexGroup } from './conditional_flex_group'; import { NetworkRoutesProps, NetworkRouteType } from './types'; import { TlsQueryTabBody } from './tls_query_tab_body'; +import { Anomaly } from '../../../components/ml/types'; export const NetworkRoutes = ({ networkPagePath, @@ -32,7 +33,7 @@ export const NetworkRoutes = ({ setAbsoluteRangeDatePicker, }: NetworkRoutesProps) => { const narrowDateRange = useCallback( - (score, interval) => { + (score: Anomaly, interval: string) => { const fromTo = scoreIntervalToDateTime(score, interval); setAbsoluteRangeDatePicker({ id: 'global', @@ -42,6 +43,12 @@ export const NetworkRoutes = ({ }, [scoreIntervalToDateTime, setAbsoluteRangeDatePicker] ); + const updateDateRange = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [from, to] + ); const tabProps = { networkPagePath, @@ -52,6 +59,7 @@ export const NetworkRoutes = ({ from, indexPattern, setQuery, + updateDateRange, }; const anomaliesProps = { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index 9682495b6f66a..5f5f0a026d375 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -14,10 +14,13 @@ import { NarrowDateRange } from '../../../components/ml/types'; import { GlobalTimeArgs } from '../../../containers/global_time'; import { SetAbsoluteRangeDatePicker } from '../types'; +import { UpdateDateRange } from '../../../components/charts/common'; interface QueryTabBodyProps { type: networkModel.NetworkType; filterQuery?: string | ESTermQuery; + updateDateRange?: UpdateDateRange; + narrowDateRange?: NarrowDateRange; } export type DnsQueryTabBodyProps = QueryTabBodyProps & GlobalTimeArgs; diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts index 84f8d004198e9..11aea7ffb07cf 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts @@ -145,11 +145,18 @@ export const networkSchema = gql` cursor: CursorType! } + type MatrixOverOrdinalHistogramData { + x: String! + y: Float! + g: String! + } + type NetworkDnsData { edges: [NetworkDnsEdges!]! totalCount: Float! pageInfo: PageInfoPaginated! inspect: Inspect + histogram: [MatrixOverOrdinalHistogramData!] } enum NetworkHttpFields { diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index d6a4d204124a1..44cfc81339527 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -1628,6 +1628,8 @@ export interface NetworkDnsData { pageInfo: PageInfoPaginated; inspect?: Maybe; + + histogram?: Maybe; } export interface NetworkDnsEdges { @@ -1650,6 +1652,14 @@ export interface NetworkDnsItem { uniqueDomains?: Maybe; } +export interface MatrixOverOrdinalHistogramData { + x: string; + + y: number; + + g: string; +} + export interface NetworkHttpData { edges: NetworkHttpEdges[]; @@ -6949,6 +6959,8 @@ export namespace NetworkDnsDataResolvers { pageInfo?: PageInfoResolver; inspect?: InspectResolver, TypeParent, TContext>; + + histogram?: HistogramResolver, TypeParent, TContext>; } export type EdgesResolver< @@ -6971,6 +6983,11 @@ export namespace NetworkDnsDataResolvers { Parent = NetworkDnsData, TContext = SiemContext > = Resolver; + export type HistogramResolver< + R = Maybe, + Parent = NetworkDnsData, + TContext = SiemContext + > = Resolver; } export namespace NetworkDnsEdgesResolvers { @@ -7039,6 +7056,32 @@ export namespace NetworkDnsItemResolvers { > = Resolver; } +export namespace MatrixOverOrdinalHistogramDataResolvers { + export interface Resolvers { + x?: XResolver; + + y?: YResolver; + + g?: GResolver; + } + + export type XResolver< + R = string, + Parent = MatrixOverOrdinalHistogramData, + TContext = SiemContext + > = Resolver; + export type YResolver< + R = number, + Parent = MatrixOverOrdinalHistogramData, + TContext = SiemContext + > = Resolver; + export type GResolver< + R = string, + Parent = MatrixOverOrdinalHistogramData, + TContext = SiemContext + > = Resolver; +} + export namespace NetworkHttpDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -8704,6 +8747,7 @@ export type IResolvers = { NetworkDnsData?: NetworkDnsDataResolvers.Resolvers; NetworkDnsEdges?: NetworkDnsEdgesResolvers.Resolvers; NetworkDnsItem?: NetworkDnsItemResolvers.Resolvers; + MatrixOverOrdinalHistogramData?: MatrixOverOrdinalHistogramDataResolvers.Resolvers; NetworkHttpData?: NetworkHttpDataResolvers.Resolvers; NetworkHttpEdges?: NetworkHttpEdgesResolvers.Resolvers; NetworkHttpItem?: NetworkHttpItemResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts index 39babc58ee138..07b748024743c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts @@ -18,6 +18,7 @@ import { NetworkHttpData, NetworkHttpEdges, NetworkTopNFlowEdges, + MatrixOverOrdinalHistogramData, } from '../../graphql/types'; import { inspectStringifyObject } from '../../utils/build_query'; import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; @@ -140,6 +141,7 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter { ); const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; const edges = networkDnsEdges.splice(cursorStart, querySize - cursorStart); + const histogram = getHistogramData(edges); const inspect = { dsl: [inspectStringifyObject(dsl)], response: [inspectStringifyObject(response)], @@ -154,6 +156,7 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter { showMorePagesIndicator, }, totalCount, + histogram, }; } @@ -194,6 +197,32 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter { } } +const getHistogramData = ( + data: NetworkDnsEdges[] +): MatrixOverOrdinalHistogramData[] | undefined => { + if (!Array.isArray(data)) return undefined; + return data.reduce( + (acc: MatrixOverOrdinalHistogramData[], { node: { dnsBytesOut, dnsBytesIn, _id } }) => { + if (_id != null && dnsBytesOut != null && dnsBytesIn != null) + return [ + ...acc, + { + x: _id, + y: dnsBytesOut, + g: 'DNS Bytes Out', + }, + { + x: _id, + y: dnsBytesIn, + g: 'DNS Bytes In', + }, + ]; + return acc; + }, + [] + ); +}; + const getTopNFlowEdges = ( response: DatabaseSearchResponse, options: NetworkTopNFlowRequestOptions diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index a22ce122b1766..e309d0e127dfc 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -9,5 +9,5 @@ "include": [ "**/*" ], - "exclude": [], + "exclude": [] } From bb222952ebb9157f22cc475b02f0b1d5d5d7cfd3 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 22 Nov 2019 18:04:27 +0100 Subject: [PATCH 030/128] [Uptime] added test for chart wrapper (#50399) * added test for chart wrapper * update unit test * updated test selector --- .../__snapshots__/chart_wrapper.test.tsx.snap | 280 ++++++++++++++++++ .../charts/__tests__/chart_wrapper.test.tsx | 83 ++++++ 2 files changed, 363 insertions(+) create mode 100644 x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap create mode 100644 x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap new file mode 100644 index 0000000000000..3f3e6b0b929e1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChartWrapper component renders the component with loading false 1`] = ` + +
    + + + +
    +
    +`; + +exports[`ChartWrapper component renders the component with loading true 1`] = ` + +
    + + + +
    + + + + + +
    +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx new file mode 100644 index 0000000000000..43e6b80d5c840 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/chart_wrapper.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { mount } from 'enzyme'; +import { nextTick } from 'test_utils/enzyme_helpers'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { ChartWrapper } from '../chart_wrapper'; +import { SnapshotHeading } from '../../snapshot_heading'; +import { DonutChart } from '../donut_chart'; +const SNAPSHOT_CHART_WIDTH = 144; +const SNAPSHOT_CHART_HEIGHT = 144; +describe('ChartWrapper component', () => { + it('renders the component with loading false', () => { + const component = shallowWithIntl( + + + + + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders the component with loading true', () => { + const component = shallowWithIntl( + + + + + + ); + expect(component).toMatchSnapshot(); + }); + + it('mounts the component with loading true or false', async () => { + const component = mount( + + + + + + ); + + let loadingChart = component.find(`.euiLoadingChart`); + expect(loadingChart.length).toBe(1); + + component.setProps({ + loading: false, + }); + await nextTick(); + component.update(); + + loadingChart = component.find(`.euiLoadingChart`); + expect(loadingChart.length).toBe(0); + }); + + it('mounts the component with chart when loading true or false', async () => { + const component = mount( + + + + + + ); + + let donutChart = component.find(DonutChart); + expect(donutChart.length).toBe(1); + + component.setProps({ + loading: false, + }); + await nextTick(); + component.update(); + + donutChart = component.find(DonutChart); + expect(donutChart.length).toBe(1); + }); +}); From 8af09faa72d7a3b3341acb8f8d8d10c55a367e9d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 22 Nov 2019 19:04:48 +0100 Subject: [PATCH 031/128] Move local application service into Kibana platform (#50661) * move local application service registry to new platform * move dev tools app itself * fix i18n * make sure legacy dev tools are imported * rename dev tools plugin --- .i18nrc.json | 1 + .../console/np_ready/public/legacy.ts | 2 +- .../console/np_ready/public/plugin.ts | 4 +- .../kibana/public/dashboard/index.ts | 2 - .../kibana/public/dashboard/plugin.ts | 14 +- .../kibana/public/dev_tools/README.md | 4 + .../kibana/public/dev_tools/index.ts | 21 ++- .../kibana/public/dev_tools/plugin.ts | 71 ---------- .../core_plugins/kibana/public/home/index.ts | 3 +- .../core_plugins/kibana/public/home/plugin.ts | 9 +- .../local_application_service.ts | 57 +------- .../new_platform/new_platform.karma_mock.js | 21 ++- .../ui/public/new_platform/new_platform.ts | 7 +- src/plugins/dev_tools/kibana.json | 5 +- .../dev_tools/public}/application.tsx | 8 +- src/plugins/dev_tools/public/plugin.ts | 20 ++- src/plugins/kibana_legacy/README.md | 6 + src/plugins/kibana_legacy/kibana.json | 6 + src/plugins/kibana_legacy/public/index.ts | 27 ++++ src/plugins/kibana_legacy/public/plugin.ts | 100 +++++++++++++ .../plugins/grokdebugger/public/register.js | 2 +- .../plugins/lens/public/app_plugin/app.tsx | 4 +- .../plugins/lens/public/app_plugin/plugin.tsx | 133 +++++++++--------- x-pack/legacy/plugins/lens/public/legacy.ts | 8 +- .../searchprofiler/public/np_ready/plugin.ts | 6 +- .../translations/translations/ja-JP.json | 6 +- .../translations/translations/zh-CN.json | 6 +- 27 files changed, 293 insertions(+), 260 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/dev_tools/README.md delete mode 100644 src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts rename src/{legacy/core_plugins/kibana/public/dev_tools => plugins/dev_tools/public}/application.tsx (94%) create mode 100644 src/plugins/kibana_legacy/README.md create mode 100644 src/plugins/kibana_legacy/kibana.json create mode 100644 src/plugins/kibana_legacy/public/index.ts create mode 100644 src/plugins/kibana_legacy/public/plugin.ts diff --git a/.i18nrc.json b/.i18nrc.json index d0d8beb6f5337..2cdf7d2b039c6 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -8,6 +8,7 @@ "embeddableApi": "src/plugins/embeddable", "share": "src/plugins/share", "esUi": "src/plugins/es_ui_shared", + "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", "inputControl": "src/legacy/core_plugins/input_control_vis", "inspector": "src/plugins/inspector", diff --git a/src/legacy/core_plugins/console/np_ready/public/legacy.ts b/src/legacy/core_plugins/console/np_ready/public/legacy.ts index b610cf7e6a3bb..758ea81be88ad 100644 --- a/src/legacy/core_plugins/console/np_ready/public/legacy.ts +++ b/src/legacy/core_plugins/console/np_ready/public/legacy.ts @@ -29,7 +29,7 @@ import { I18nContext } from 'ui/i18n'; /* eslint-enable @kbn/eslint/no-restricted-paths */ export interface XPluginSet { - devTools: DevToolsSetup; + dev_tools: DevToolsSetup; home: HomePublicPluginSetup; __LEGACY: { I18nContext: any; diff --git a/src/legacy/core_plugins/console/np_ready/public/plugin.ts b/src/legacy/core_plugins/console/np_ready/public/plugin.ts index 4050f20a4fb07..37758adc98d11 100644 --- a/src/legacy/core_plugins/console/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/console/np_ready/public/plugin.ts @@ -31,7 +31,7 @@ export class ConsoleUIPlugin implements Plugin { async setup({ notifications }: CoreSetup, pluginSet: XPluginSet) { const { __LEGACY: { I18nContext }, - devTools, + dev_tools, home, } = pluginSet; @@ -49,7 +49,7 @@ export class ConsoleUIPlugin implements Plugin { category: FeatureCatalogueCategory.ADMIN, }); - devTools.register({ + dev_tools.register({ id: 'console', order: 1, title: i18n.translate('console.consoleDisplayName', { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/index.ts index d134739aa24c2..d37cf5d7139ec 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.ts @@ -26,7 +26,6 @@ import { } from './legacy_imports'; import { DashboardPlugin, LegacyAngularInjectedDependencies } from './plugin'; import { start as data } from '../../../data/public/legacy'; -import { localApplicationService } from '../local_application_service'; import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; import { start as navigation } from '../../../navigation/public/legacy'; import './saved_dashboard/saved_dashboards'; @@ -55,7 +54,6 @@ async function getAngularDependencies(): Promise Promise; - localApplicationService: LocalApplicationService; }; home: HomePublicPluginSetup; + kibana_legacy: KibanaLegacySetup; } export class DashboardPlugin implements Plugin { @@ -74,10 +74,7 @@ export class DashboardPlugin implements Plugin { public setup( core: CoreSetup, - { - __LEGACY: { localApplicationService, getAngularDependencies, ...legacyServices }, - home, - }: DashboardPluginSetupDependencies + { __LEGACY: { getAngularDependencies }, home, kibana_legacy }: DashboardPluginSetupDependencies ) { const app: App = { id: '', @@ -97,7 +94,6 @@ export class DashboardPlugin implements Plugin { const angularDependencies = await getAngularDependencies(); const deps: RenderDeps = { core: contextCore as LegacyCoreStart, - ...legacyServices, ...angularDependencies, navigation, dataStart, @@ -117,8 +113,8 @@ export class DashboardPlugin implements Plugin { return renderApp(params.element, params.appBasePath, deps); }, }; - localApplicationService.register({ ...app, id: 'dashboard' }); - localApplicationService.register({ ...app, id: 'dashboards' }); + kibana_legacy.registerLegacyApp({ ...app, id: 'dashboard' }); + kibana_legacy.registerLegacyApp({ ...app, id: 'dashboards' }); home.featureCatalogue.register({ id: 'dashboard', diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/README.md b/src/legacy/core_plugins/kibana/public/dev_tools/README.md new file mode 100644 index 0000000000000..0234830d6071c --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dev_tools/README.md @@ -0,0 +1,4 @@ +This folder is just a left-over of the things that can't be moved to Kibana platform just yet: + +* Styling (this can be moved as soon as there is support for styling in Kibana platform) +* Check whether there are no dev tools and hide the link in the nav bar (this can be moved as soon as all dev tools are moved) \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/index.ts b/src/legacy/core_plugins/kibana/public/dev_tools/index.ts index 74708e36a98aa..f2555259028cc 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/index.ts +++ b/src/legacy/core_plugins/kibana/public/dev_tools/index.ts @@ -17,18 +17,13 @@ * under the License. */ -import { npSetup, npStart } from 'ui/new_platform'; +// make sure all dev tools are loaded and registered. +import 'uiExports/devTools'; -import { DevToolsPlugin } from './plugin'; -import { localApplicationService } from '../local_application_service'; +import { npStart } from 'ui/new_platform'; -const instance = new DevToolsPlugin(); - -instance.setup(npSetup.core, { - __LEGACY: { - localApplicationService, - }, -}); -instance.start(npStart.core, { - newPlatformDevTools: npStart.plugins.devTools, -}); +if (npStart.plugins.dev_tools.getSortedDevTools().length === 0) { + npStart.core.chrome.navLinks.update('kibana:dev_tools', { + hidden: true, + }); +} diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts b/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts deleted file mode 100644 index ec9af1a6acd92..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// This import makes sure dev tools are registered before the app is. -import 'uiExports/devTools'; - -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; - -import { LocalApplicationService } from '../local_application_service'; -import { DevTool, DevToolsStart } from '../../../../../plugins/dev_tools/public'; - -export interface DevToolsPluginSetupDependencies { - __LEGACY: { - localApplicationService: LocalApplicationService; - }; -} - -export interface DevToolsPluginStartDependencies { - newPlatformDevTools: DevToolsStart; -} - -export class DevToolsPlugin implements Plugin { - private getSortedDevTools: (() => readonly DevTool[]) | null = null; - - public setup( - core: CoreSetup, - { __LEGACY: { localApplicationService } }: DevToolsPluginSetupDependencies - ) { - localApplicationService.register({ - id: 'dev_tools', - title: 'Dev Tools', - mount: async (appMountContext, params) => { - if (!this.getSortedDevTools) { - throw new Error('not started yet'); - } - const { renderApp } = await import('./application'); - return renderApp( - params.element, - appMountContext, - params.appBasePath, - this.getSortedDevTools() - ); - }, - }); - } - - public start(core: CoreStart, { newPlatformDevTools }: DevToolsPluginStartDependencies) { - this.getSortedDevTools = newPlatformDevTools.getSortedDevTools; - if (this.getSortedDevTools().length === 0) { - core.chrome.navLinks.update('kibana:dev_tools', { - hidden: true, - }); - } - } -} diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index b1c03507c9a2d..6494cc79640e1 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -25,7 +25,6 @@ import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; import { start as data } from '../../../data/public/legacy'; import { TelemetryOptInProvider } from '../../../telemetry/public/services'; -import { localApplicationService } from '../local_application_service'; export const trackUiMetric = createUiStatsReporter('Kibana_home'); @@ -54,6 +53,7 @@ let copiedLegacyCatalogue = false; (async () => { const instance = new HomePlugin(); instance.setup(npSetup.core, { + ...npSetup.plugins, __LEGACY: { trackUiMetric, metadata: npStart.core.injectedMetadata.getLegacyMetadata(), @@ -71,7 +71,6 @@ let copiedLegacyCatalogue = false; return npStart.plugins.home.featureCatalogue.get(); }, getAngularDependencies, - localApplicationService, }, }); instance.start(npStart.core, { diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 18e101fc58d51..bb0e7e3611616 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -21,8 +21,8 @@ import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'ki import { UiStatsMetricType } from '@kbn/analytics'; import { DataStart } from '../../../data/public'; -import { LocalApplicationService } from '../local_application_service'; import { setServices } from './kibana_services'; +import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; import { FeatureCatalogueEntry } from '../../../../../plugins/home/public'; export interface LegacyAngularInjectedDependencies { @@ -53,8 +53,8 @@ export interface HomePluginSetupDependencies { }; getFeatureCatalogueEntries: () => Promise; getAngularDependencies: () => Promise; - localApplicationService: LocalApplicationService; }; + kibana_legacy: KibanaLegacySetup; } export class HomePlugin implements Plugin { @@ -64,10 +64,11 @@ export class HomePlugin implements Plugin { setup( core: CoreSetup, { - __LEGACY: { localApplicationService, getAngularDependencies, ...legacyServices }, + kibana_legacy, + __LEGACY: { getAngularDependencies, ...legacyServices }, }: HomePluginSetupDependencies ) { - localApplicationService.register({ + kibana_legacy.registerLegacyApp({ id: 'home', title: 'Home', mount: async ({ core: contextCore }, params) => { diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index 9d87e187fd1e1..e5bfd88ea7637 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -23,12 +23,6 @@ import { ILocationService, IScope } from 'angular'; import { npStart } from 'ui/new_platform'; import { htmlIdGenerator } from '@elastic/eui'; -interface ForwardDefinition { - legacyAppId: string; - newAppId: string; - keepPrefix: boolean; -} - const matchAllWithPrefix = (prefixOrApp: string | App) => `/${typeof prefixOrApp === 'string' ? prefixOrApp : prefixOrApp.id}/:tail*?`; @@ -45,55 +39,8 @@ const matchAllWithPrefix = (prefixOrApp: string | App) => * router that handles switching between applications without page reload. */ export class LocalApplicationService { - private apps: App[] = []; - private forwards: ForwardDefinition[] = []; private idGenerator = htmlIdGenerator('kibanaAppLocalApp'); - /** - * Register an app to be managed by the application service. - * This method works exactly as `core.application.register`. - * - * When an app is mounted, it is responsible for routing. The app - * won't be mounted again if the route changes within the prefix - * of the app (its id). It is fine to use whatever means for handling - * routing within the app. - * - * When switching to a URL outside of the current prefix, the app router - * shouldn't do anything because it doesn't own the routing anymore - - * the local application service takes over routing again, - * unmounts the current app and mounts the next app. - * - * @param app The app descriptor - */ - register(app: App) { - this.apps.push(app); - } - - /** - * Forwards every URL starting with `legacyAppId` to the same URL starting - * with `newAppId` - e.g. `/legacy/my/legacy/path?q=123` gets forwarded to - * `/newApp/my/legacy/path?q=123`. - * - * When setting the `keepPrefix` option, the new app id is simply prepended. - * The example above would become `/newApp/legacy/my/legacy/path?q=123`. - * - * This method can be used to provide backwards compatibility for URLs when - * renaming or nesting plugins. For route changes after the prefix, please - * use the routing mechanism of your app. - * - * @param legacyAppId The name of the old app to forward URLs from - * @param newAppId The name of the new app that handles the URLs now - * @param options Whether the prefix of the old app is kept to nest the legacy - * path into the new path - */ - forwardApp( - legacyAppId: string, - newAppId: string, - options: { keepPrefix: boolean } = { keepPrefix: false } - ) { - this.forwards.push({ legacyAppId, newAppId, ...options }); - } - /** * Wires up listeners to handle mounting and unmounting of apps to * the legacy angular route manager. Once all apps within the Kibana @@ -103,7 +50,7 @@ export class LocalApplicationService { * @param angularRouteManager The current `ui/routes` instance */ attachToAngular(angularRouteManager: UIRoutes) { - this.apps.forEach(app => { + npStart.plugins.kibana_legacy.getApps().forEach(app => { const wrapperElementId = this.idGenerator(); angularRouteManager.when(matchAllWithPrefix(app), { outerAngularWrapperRoute: true, @@ -131,7 +78,7 @@ export class LocalApplicationService { }); }); - this.forwards.forEach(({ legacyAppId, newAppId, keepPrefix }) => { + npStart.plugins.kibana_legacy.getForwards().forEach(({ legacyAppId, newAppId, keepPrefix }) => { angularRouteManager.when(matchAllWithPrefix(legacyAppId), { resolveRedirectTo: ($location: ILocationService) => { const url = $location.url(); diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 8703e8750fda4..773d4283cad88 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -22,7 +22,7 @@ import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_fiel const mockObservable = () => { return { - subscribe: () => {} + subscribe: () => {}, }; }; @@ -75,16 +75,20 @@ export const npSetup = { timefilter: { timefilter: sinon.fake(), history: sinon.fake(), - } + }, }, fieldFormats: getFieldFormatsRegistry(mockUiSettings), }, share: { register: () => {}, }, - devTools: { + dev_tools: { register: () => {}, }, + kibana_legacy: { + registerLegacyApp: () => {}, + forwardApp: () => {}, + }, inspector: { registerView: () => undefined, __LEGACY: { @@ -110,7 +114,7 @@ let isAutoRefreshSelectorEnabled = true; export const npStart = { core: { - chrome: {} + chrome: {}, }, plugins: { embeddable: { @@ -123,9 +127,13 @@ export const npStart = { registerRenderer: sinon.fake(), registerType: sinon.fake(), }, - devTools: { + dev_tools: { getSortedDevTools: () => [], }, + kibana_legacy: { + getApps: () => [], + getForwards: () => [], + }, data: { autocomplete: { getProvider: sinon.fake(), @@ -142,7 +150,6 @@ export const npStart = { setFilters: sinon.fake(), removeAll: sinon.fake(), getUpdates$: mockObservable, - }, timefilter: { timefilter: { @@ -166,7 +173,7 @@ export const npStart = { getRefreshInterval: () => { return refreshInterval; }, - setRefreshInterval: (interval) => { + setRefreshInterval: interval => { refreshInterval = interval; }, enableTimeRangeSelector: () => { diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 1db360749c714..acf1191852dc8 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -29,6 +29,7 @@ import { } from '../../../../plugins/inspector/public'; import { EuiUtilsStart } from '../../../../plugins/eui_utils/public'; import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/public'; +import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public'; import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public'; import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; @@ -39,8 +40,9 @@ export interface PluginsSetup { home: HomePublicPluginSetup; inspector: InspectorSetup; uiActions: IUiActionsSetup; + dev_tools: DevToolsSetup; + kibana_legacy: KibanaLegacySetup; share: SharePluginSetup; - devTools: DevToolsSetup; } export interface PluginsStart { @@ -51,8 +53,9 @@ export interface PluginsStart { home: HomePublicPluginStart; inspector: InspectorStart; uiActions: IUiActionsStart; + dev_tools: DevToolsStart; + kibana_legacy: KibanaLegacyStart; share: SharePluginStart; - devTools: DevToolsStart; } export const npSetup = { diff --git a/src/plugins/dev_tools/kibana.json b/src/plugins/dev_tools/kibana.json index 307035c7ec664..df7b1e3a781b0 100644 --- a/src/plugins/dev_tools/kibana.json +++ b/src/plugins/dev_tools/kibana.json @@ -1,6 +1,7 @@ { - "id": "devTools", + "id": "dev_tools", "version": "kibana", "server": false, - "ui": true + "ui": true, + "requiredPlugins": ["kibana_legacy"] } diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/application.tsx b/src/plugins/dev_tools/public/application.tsx similarity index 94% rename from src/legacy/core_plugins/kibana/public/dev_tools/application.tsx rename to src/plugins/dev_tools/public/application.tsx index 3945d8d8dc856..b3c6bb592f378 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -26,7 +26,7 @@ import ReactDOM from 'react-dom'; import { useEffect, useRef } from 'react'; import { AppMountContext } from 'kibana/public'; -import { DevTool } from '../../../../../plugins/dev_tools/public'; +import { DevTool } from './plugin'; interface DevToolsWrapperProps { devTools: readonly DevTool[]; @@ -120,10 +120,10 @@ function setBadge(appMountContext: AppMountContext) { return; } appMountContext.core.chrome.setBadge({ - text: i18n.translate('kbn.devTools.badge.readOnly.text', { + text: i18n.translate('devTools.badge.readOnly.text', { defaultMessage: 'Read only', }), - tooltip: i18n.translate('kbn.devTools.badge.readOnly.tooltip', { + tooltip: i18n.translate('devTools.badge.readOnly.tooltip', { defaultMessage: 'Unable to save', }), iconType: 'glasses', @@ -133,7 +133,7 @@ function setBadge(appMountContext: AppMountContext) { function setBreadcrumbs(appMountContext: AppMountContext) { appMountContext.core.chrome.setBreadcrumbs([ { - text: i18n.translate('kbn.devTools.k7BreadcrumbsDevToolsLabel', { + text: i18n.translate('devTools.k7BreadcrumbsDevToolsLabel', { defaultMessage: 'Dev Tools', }), href: '#/dev_tools', diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 8098308c0882b..124c00c755904 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -19,6 +19,7 @@ import { App, CoreSetup, Plugin } from 'kibana/public'; import { sortBy } from 'lodash'; +import { KibanaLegacySetup } from '../../kibana_legacy/public'; export interface DevToolsSetup { /** @@ -93,7 +94,24 @@ export class DevToolsPlugin implements Plugin { return sortBy([...this.devTools.values()], 'order'); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { kibana_legacy }: { kibana_legacy: KibanaLegacySetup }) { + kibana_legacy.registerLegacyApp({ + id: 'dev_tools', + title: 'Dev Tools', + mount: async (appMountContext, params) => { + if (!this.getSortedDevTools) { + throw new Error('not started yet'); + } + const { renderApp } = await import('./application'); + return renderApp( + params.element, + appMountContext, + params.appBasePath, + this.getSortedDevTools() + ); + }, + }); + return { register: (devTool: DevTool) => { if (this.devTools.has(devTool.id)) { diff --git a/src/plugins/kibana_legacy/README.md b/src/plugins/kibana_legacy/README.md new file mode 100644 index 0000000000000..82bf3270589db --- /dev/null +++ b/src/plugins/kibana_legacy/README.md @@ -0,0 +1,6 @@ +# kibana-legacy + +This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. + +Currently, the only service offered is the ability to register apps which are rendered in the legacy "kibana" plugin. + diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json new file mode 100644 index 0000000000000..26ee6db3ba06a --- /dev/null +++ b/src/plugins/kibana_legacy/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "kibana_legacy", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts new file mode 100644 index 0000000000000..4cb30be8917ac --- /dev/null +++ b/src/plugins/kibana_legacy/public/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { KibanaLegacyPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new KibanaLegacyPlugin(); +} + +export * from './plugin'; diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts new file mode 100644 index 0000000000000..cb95088320d7b --- /dev/null +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { App } from 'kibana/public'; + +interface ForwardDefinition { + legacyAppId: string; + newAppId: string; + keepPrefix: boolean; +} + +export class KibanaLegacyPlugin { + private apps: App[] = []; + private forwards: ForwardDefinition[] = []; + + public setup() { + return { + /** + * @deprecated + * Register an app to be managed by the application service. + * This method works exactly as `core.application.register`. + * + * When an app is mounted, it is responsible for routing. The app + * won't be mounted again if the route changes within the prefix + * of the app (its id). It is fine to use whatever means for handling + * routing within the app. + * + * When switching to a URL outside of the current prefix, the app router + * shouldn't do anything because it doesn't own the routing anymore - + * the local application service takes over routing again, + * unmounts the current app and mounts the next app. + * + * @param app The app descriptor + */ + registerLegacyApp: (app: App) => { + this.apps.push(app); + }, + + /** + * @deprecated + * Forwards every URL starting with `legacyAppId` to the same URL starting + * with `newAppId` - e.g. `/legacy/my/legacy/path?q=123` gets forwarded to + * `/newApp/my/legacy/path?q=123`. + * + * When setting the `keepPrefix` option, the new app id is simply prepended. + * The example above would become `/newApp/legacy/my/legacy/path?q=123`. + * + * This method can be used to provide backwards compatibility for URLs when + * renaming or nesting plugins. For route changes after the prefix, please + * use the routing mechanism of your app. + * + * @param legacyAppId The name of the old app to forward URLs from + * @param newAppId The name of the new app that handles the URLs now + * @param options Whether the prefix of the old app is kept to nest the legacy + * path into the new path + */ + forwardApp: ( + legacyAppId: string, + newAppId: string, + options: { keepPrefix: boolean } = { keepPrefix: false } + ) => { + this.forwards.push({ legacyAppId, newAppId, ...options }); + }, + }; + } + + public start() { + return { + /** + * @deprecated + * Just exported for wiring up with legacy platform, should not be used. + */ + getApps: () => this.apps, + /** + * @deprecated + * Just exported for wiring up with legacy platform, should not be used. + */ + getForwards: () => this.forwards, + }; + } +} + +export type KibanaLegacySetup = ReturnType; +export type KibanaLegacyStart = ReturnType; diff --git a/x-pack/legacy/plugins/grokdebugger/public/register.js b/x-pack/legacy/plugins/grokdebugger/public/register.js index 74679d65e52d2..8201fed5b2220 100644 --- a/x-pack/legacy/plugins/grokdebugger/public/register.js +++ b/x-pack/legacy/plugins/grokdebugger/public/register.js @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { npSetup, npStart } from 'ui/new_platform'; -npSetup.plugins.devTools.register({ +npSetup.plugins.dev_tools.register({ order: 6, title: i18n.translate('xpack.grokDebugger.displayName', { defaultMessage: 'Grok Debugger', diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 553b98643f8a5..c29b0df9ee9fa 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -10,7 +10,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; -import { CoreStart, NotificationsStart } from 'src/core/public'; +import { AppMountContext, NotificationsStart } from 'src/core/public'; import { DataStart, IndexPattern as IndexPatternInstance, @@ -55,7 +55,7 @@ export function App({ }: { editorFrame: EditorFrameInstance; data: DataPublicPluginStart; - core: CoreStart; + core: AppMountContext['core']; dataShim: DataStart; storage: IStorageWrapper; docId?: string; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 60a375f696f7b..93f5928f58aa1 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -17,8 +17,7 @@ import React from 'react'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom'; import { render, unmountComponentAtNode } from 'react-dom'; -import chrome from 'ui/chrome'; -import { CoreSetup, CoreStart } from 'src/core/public'; +import { CoreSetup, CoreStart, SavedObjectsClientContract } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; @@ -33,30 +32,35 @@ import { datatableVisualizationStop, } from '../datatable_visualization_plugin'; import { App } from './app'; -import { EditorFrameInstance } from '../types'; import { LensReportManager, setReportManager, stopReportManager, trackUiEvent, } from '../lens_ui_telemetry'; -import { LocalApplicationService } from '../../../../../../src/legacy/core_plugins/kibana/public/local_application_service'; import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../index'; +import { KibanaLegacySetup } from '../../../../../../src/plugins/kibana_legacy/public'; +import { EditorFrameStart } from '../types'; + +export interface LensPluginSetupDependencies { + kibana_legacy: KibanaLegacySetup; +} export interface LensPluginStartDependencies { data: DataPublicPluginStart; dataShim: DataStart; - __LEGACY: { - localApplicationService: LocalApplicationService; - }; } export class AppPlugin { - private instance: EditorFrameInstance | null = null; - private store: SavedObjectIndexStore | null = null; + private startDependencies: { + data: DataPublicPluginStart; + dataShim: DataStart; + savedObjectsClient: SavedObjectsClientContract; + editorFrame: EditorFrameStart; + } | null = null; constructor() {} - setup(core: CoreSetup, plugins: {}) { + setup(core: CoreSetup, { kibana_legacy }: LensPluginSetupDependencies) { // TODO: These plugins should not be called from the top level, but since this is the // entry point to the app we have no choice until the new platform is ready const indexPattern = indexPatternDatasourceSetup(); @@ -64,68 +68,57 @@ export class AppPlugin { const xyVisualization = xyVisualizationSetup(); const metricVisualization = metricVisualizationSetup(); const editorFrameSetupInterface = editorFrameSetup(); - this.store = new SavedObjectIndexStore(chrome!.getSavedObjectsClient()); editorFrameSetupInterface.registerVisualization(xyVisualization); editorFrameSetupInterface.registerVisualization(datatableVisualization); editorFrameSetupInterface.registerVisualization(metricVisualization); editorFrameSetupInterface.registerDatasource('indexpattern', indexPattern); - } - - start( - core: CoreStart, - { data, dataShim, __LEGACY: { localApplicationService } }: LensPluginStartDependencies - ) { - if (this.store === null) { - throw new Error('Start lifecycle called before setup lifecycle'); - } - - addHelpMenuToAppChrome(core.chrome); - - const store = this.store; - - const editorFrameStartInterface = editorFrameStart(); - - this.instance = editorFrameStartInterface.createInstance({}); - - setReportManager( - new LensReportManager({ - storage: new Storage(localStorage), - http: core.http, - }) - ); - - const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { - trackUiEvent('loaded'); - return ( - { - if (!id) { - routeProps.history.push('/lens'); - } else { - routeProps.history.push(`/lens/edit/${id}`); - } - }} - /> - ); - }; - - function NotFound() { - trackUiEvent('loaded_404'); - return ; - } - localApplicationService.register({ + kibana_legacy.registerLegacyApp({ id: 'lens', title: NOT_INTERNATIONALIZED_PRODUCT_NAME, mount: async (context, params) => { + if (this.startDependencies === null) { + throw new Error('mounted before start phase'); + } + const { data, dataShim, savedObjectsClient, editorFrame } = this.startDependencies; + addHelpMenuToAppChrome(context.core.chrome); + + const instance = editorFrame.createInstance({}); + + setReportManager( + new LensReportManager({ + storage: new Storage(localStorage), + http: core.http, + }) + ); + + const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { + trackUiEvent('loaded'); + return ( + { + if (!id) { + routeProps.history.push('/lens'); + } else { + routeProps.history.push(`/lens/edit/${id}`); + } + }} + /> + ); + }; + + function NotFound() { + trackUiEvent('loaded_404'); + return ; + } render( @@ -139,17 +132,23 @@ export class AppPlugin { params.element ); return () => { + instance.unmount(); unmountComponentAtNode(params.element); }; }, }); } - stop() { - if (this.instance) { - this.instance.unmount(); - } + start({ savedObjects }: CoreStart, { data, dataShim }: LensPluginStartDependencies) { + this.startDependencies = { + data, + dataShim, + savedObjectsClient: savedObjects.client, + editorFrame: editorFrameStart(), + }; + } + stop() { stopReportManager(); // TODO this will be handled by the plugin platform itself diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts index 0285e041de78c..a39d73f187ece 100644 --- a/x-pack/legacy/plugins/lens/public/legacy.ts +++ b/x-pack/legacy/plugins/lens/public/legacy.ts @@ -9,15 +9,11 @@ import { start as dataShimStart } from '../../../../../src/legacy/core_plugins/d export * from './types'; -import { localApplicationService } from '../../../../../src/legacy/core_plugins/kibana/public/local_application_service'; import { AppPlugin } from './app_plugin'; const app = new AppPlugin(); -app.setup(npSetup.core, {}); +app.setup(npSetup.core, npSetup.plugins); app.start(npStart.core, { + ...npStart.plugins, dataShim: dataShimStart, - data: npStart.plugins.data, - __LEGACY: { - localApplicationService, - }, }); diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts index 7a5981fcb0a69..f2acc97e55a70 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts @@ -27,15 +27,15 @@ export class SearchProfilerUIPlugin implements Plugin { notifications: ToastsStart; formatAngularHttpError: any; }; - devTools: DevToolsSetup; + dev_tools: DevToolsSetup; } ) { const { http } = core; const { __LEGACY: { I18nContext, licenseEnabled, notifications, formatAngularHttpError }, - devTools, + dev_tools, } = plugins; - devTools.register({ + dev_tools.register({ id: 'searchprofiler', title: i18n.translate('xpack.searchProfiler.pageDisplayName', { defaultMessage: 'Search Profiler', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a8978465b7b75..3dd141a164e3e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -872,6 +872,9 @@ "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} の説明", "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "選択されたクエリボタン {savedQueryName} を保存しました。変更を破棄するには押してください。", "data.search.searchBar.savedQueryPopoverTitleText": "保存されたクエリ", + "devTools.badge.readOnly.text": "読み込み専用", + "devTools.badge.readOnly.tooltip": "を保存できませんでした", + "devTools.k7BreadcrumbsDevToolsLabel": "開発ツール", "data.filter.filterEditor.operatorSelectPlaceholderSelect": "選択してください", "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "待機中", "data.filter.filterEditor.rangeInputLabel": "範囲", @@ -1507,9 +1510,6 @@ "kbn.dashboard.urlWasRemovedInSixZeroWarningMessage": "URL「dashboard/create」は 6.0 で廃止されました。ブックマークを更新してください。", "kbn.dashboard.visitVisualizeAppLinkText": "可視化アプリにアクセス", "kbn.dashboardTitle": "ダッシュボード", - "kbn.devTools.badge.readOnly.text": "読み込み専用", - "kbn.devTools.badge.readOnly.tooltip": "を保存できませんでした", - "kbn.devTools.k7BreadcrumbsDevToolsLabel": "開発ツール", "kbn.devToolsTitle": "開発ツール", "kbn.discover.backToTopLinkText": "最上部へ戻る。", "kbn.discover.badge.readOnly.text": "読み込み専用", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2925c777f84d2..fb5b7cc191e61 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -873,6 +873,9 @@ "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} 描述", "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "已保存查询按钮已选择 {savedQueryName}。按下可清除任何更改。", "data.search.searchBar.savedQueryPopoverTitleText": "已保存查询", + "devTools.badge.readOnly.text": "只读", + "devTools.badge.readOnly.tooltip": "无法保存", + "devTools.k7BreadcrumbsDevToolsLabel": "开发工具", "data.filter.filterEditor.operatorSelectPlaceholderSelect": "选择", "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "正在等候", "data.filter.filterEditor.rangeInputLabel": "范围", @@ -1508,9 +1511,6 @@ "kbn.dashboard.urlWasRemovedInSixZeroWarningMessage": "6.0 中未移除 url“dashboard/create”。请更新您的书签。", "kbn.dashboard.visitVisualizeAppLinkText": "访问 Visualize 应用", "kbn.dashboardTitle": "仪表板", - "kbn.devTools.badge.readOnly.text": "只读", - "kbn.devTools.badge.readOnly.tooltip": "无法保存", - "kbn.devTools.k7BreadcrumbsDevToolsLabel": "开发工具", "kbn.devToolsTitle": "开发工具", "kbn.discover.backToTopLinkText": "返至顶部。", "kbn.discover.badge.readOnly.text": "只读", From 9747a0f929562829867f053d0a1100f6e0417ef0 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 22 Nov 2019 11:33:09 -0700 Subject: [PATCH 032/128] skip flaky suite (#51479) --- src/legacy/server/config/__tests__/deprecation_warnings.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js index 7cf6fdcd85ad5..3cebc730e66de 100644 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ b/src/legacy/server/config/__tests__/deprecation_warnings.js @@ -25,7 +25,8 @@ const RUN_KBN_SERVER_STARTUP = require.resolve('./fixtures/run_kbn_server_startu const SETUP_NODE_ENV = require.resolve('../../../../setup_node_env'); const SECOND = 1000; -describe('config/deprecation warnings', function () { +// FLAKY: https://github.com/elastic/kibana/issues/51479 +describe.skip('config/deprecation warnings', function () { this.timeout(15 * SECOND); let stdio = ''; From fab4a5064abd79a70dc18dec0d629c5903a8b266 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 22 Nov 2019 15:14:36 -0700 Subject: [PATCH 033/128] [Maps] focus inputs when editor opens popovers (#51487) --- .../__snapshots__/add_tooltip_field_popover.test.js.snap | 4 ++-- .../maps/public/components/add_tooltip_field_popover.js | 1 + .../layer_panel/filter_editor/filter_editor.js | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap index 968915905cd31..f37dfdd879c5b 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/add_tooltip_field_popover.test.js.snap @@ -22,7 +22,7 @@ exports[`Should remove selected fields from selectable 1`] = ` hasArrow={true} id="addTooltipFieldPopover" isOpen={false} - ownFocus={false} + ownFocus={true} panelPaddingSize="m" > {this._renderContent()} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 20520ad3ff8f1..0086c5067ba12 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -90,6 +90,7 @@ export class FilterEditor extends Component { isOpen={this.state.isPopoverOpen} closePopover={this._close} anchorPosition="leftCenter" + ownFocus >
    Date: Fri, 22 Nov 2019 17:16:41 -0500 Subject: [PATCH 034/128] [SIEM] [DETECTION ENGINE] Add creation rule (#51376) * Add creation rule on Detection Engine * review + bug fixes * review II + clean up * fix persistence saved query * fix eui prop + add type security to add rule * fix more bug from review III * review IV --- .../static/forms/components/field.tsx | 6 + .../components/fields/checkbox_field.tsx | 3 +- .../components/fields/combobox_field.tsx | 1 + .../static/forms/components/fields/index.ts | 2 + .../components/fields/multi_select_field.tsx | 3 +- .../forms/components/fields/numeric_field.tsx | 3 +- .../components/fields/radio_group_field.tsx | 54 +++++ .../forms/components/fields/range_field.tsx | 68 ++++++ .../forms/components/fields/select_field.tsx | 6 +- .../components/fields/text_area_field.tsx | 3 +- .../forms/components/fields/text_field.tsx | 3 +- .../forms/components/fields/toggle_field.tsx | 3 +- .../static/forms/hook_form_lib/constants.ts | 3 + .../{components => hook_form_lib}/helpers.ts | 2 +- .../forms/hook_form_lib/hooks/use_field.ts | 2 + .../static/forms/hook_form_lib/index.ts | 1 + .../static/forms/hook_form_lib/types.ts | 2 + .../public/components/header_global/index.tsx | 2 +- .../public/components/header_page/index.tsx | 15 +- .../components/query_bar/index.test.tsx | 2 + .../public/components/query_bar/index.tsx | 3 + .../containers/detection_engine/rules/api.ts | 31 ++- .../rules/fetch_index_patterns.tsx | 84 ++++++++ .../detection_engine/rules/persist_rule.tsx | 64 ++++++ .../detection_engine/rules/translations.ts | 7 + .../detection_engine/rules/types.ts | 35 ++++ .../siem/public/containers/source/index.tsx | 60 +++++- .../siem/public/lib/theme/use_eui_theme.tsx | 16 ++ .../components/accordion_title/index.tsx | 27 +++ .../components/add_item_form/index.tsx | 129 ++++++++++++ .../components/add_item_form/translations.ts | 14 ++ .../components/query_bar/index.tsx | 193 ++++++++++++++++++ .../components/schedule_item_form/index.tsx | 112 ++++++++++ .../schedule_item_form/translations.ts | 35 ++++ .../create_rule/components/shared_imports.ts | 16 ++ .../components/status_icon/index.tsx | 38 ++++ .../components/step_about_rule/data.ts | 28 +++ .../step_about_rule/default_value.ts | 15 ++ .../components/step_about_rule/index.tsx | 145 +++++++++++++ .../components/step_about_rule/schema.tsx | 116 +++++++++++ .../step_about_rule/translations.ts | 49 +++++ .../components/step_define_rule/index.tsx | 159 +++++++++++++++ .../components/step_define_rule/schema.tsx | 123 +++++++++++ .../step_define_rule/translations.tsx | 35 ++++ .../components/step_define_rule/types.ts | 11 + .../components/step_schedule_rule/index.tsx | 85 ++++++++ .../components/step_schedule_rule/schema.tsx | 46 +++++ .../step_schedule_rule/translations.tsx | 21 ++ .../detection_engine/create_rule/helpers.ts | 99 +++++++++ .../detection_engine/create_rule/index.tsx | 169 ++++++++++++++- .../create_rule/translations.ts | 29 +++ .../detection_engine/create_rule/types.ts | 70 +++++++ 52 files changed, 2221 insertions(+), 27 deletions(-) create mode 100644 src/plugins/es_ui_shared/static/forms/components/fields/radio_group_field.tsx create mode 100644 src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx rename src/plugins/es_ui_shared/static/forms/{components => hook_form_lib}/helpers.ts (96%) create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx create mode 100644 x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts diff --git a/src/plugins/es_ui_shared/static/forms/components/field.tsx b/src/plugins/es_ui_shared/static/forms/components/field.tsx index 3f4050e98f64d..89dea53d75b38 100644 --- a/src/plugins/es_ui_shared/static/forms/components/field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/field.tsx @@ -29,20 +29,26 @@ interface Props { import { TextField, + TextAreaField, NumericField, CheckBoxField, ComboBoxField, MultiSelectField, + RadioGroupField, + RangeField, SelectField, ToggleField, } from './fields'; const mapTypeToFieldComponent = { [FIELD_TYPES.TEXT]: TextField, + [FIELD_TYPES.TEXTAREA]: TextAreaField, [FIELD_TYPES.NUMBER]: NumericField, [FIELD_TYPES.CHECKBOX]: CheckBoxField, [FIELD_TYPES.COMBO_BOX]: ComboBoxField, [FIELD_TYPES.MULTI_SELECT]: MultiSelectField, + [FIELD_TYPES.RADIO_GROUP]: RadioGroupField, + [FIELD_TYPES.RANGE]: RangeField, [FIELD_TYPES.SELECT]: SelectField, [FIELD_TYPES.TOGGLE]: ToggleField, }; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx index 73e8c1ff1426b..0443b4ff09e60 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx @@ -21,8 +21,7 @@ import React from 'react'; import { EuiFormRow, EuiCheckbox } from '@elastic/eui'; import uuid from 'uuid'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx index 5c2e4a4165d5f..fa73e5a663863 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx @@ -82,6 +82,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => return ( ; + idAria?: string; + [key: string]: any; +} + +export const RadioGroupField = ({ field, euiFieldProps = {}, ...rest }: Props) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx new file mode 100644 index 0000000000000..4ed2dd40968e5 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback } from 'react'; +import { EuiFormRow, EuiRange } from '@elastic/eui'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; + +interface Props { + field: FieldHook; + euiFieldProps?: Record; + idAria?: string; + [key: string]: any; +} + +export const RangeField = ({ field, euiFieldProps = {}, ...rest }: Props) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const onChange = useCallback( + (e: React.ChangeEvent | React.MouseEvent) => { + const event = ({ ...e, value: `${e.currentTarget.value}` } as unknown) as React.ChangeEvent<{ + value: string; + }>; + field.onChange(event); + }, + [field.onChange] + ); + + return ( + + + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx index b7eb1c5fa3bd3..a6d77e3b179ed 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/select_field.tsx @@ -20,8 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; @@ -49,10 +48,11 @@ export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => { onChange={e => { field.setValue(e.target.value); }} + options={[]} hasNoInitialSelection={true} isInvalid={isInvalid} data-test-subj="select" - {...(euiFieldProps as { options: any; [key: string]: any })} + {...euiFieldProps} /> ); diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx index 6916f224f8bda..b9c6424a00656 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx @@ -20,8 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiTextArea } from '@elastic/eui'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/text_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/text_field.tsx index 6e1bc639e65ce..9e255d8eda22c 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/text_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/text_field.tsx @@ -20,8 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx index 0c075c497a4d0..c6d89d0bfde21 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx @@ -20,8 +20,7 @@ import React from 'react'; import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; -import { FieldHook } from '../../hook_form_lib'; -import { getFieldValidityAndErrorMessage } from '../helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { field: FieldHook; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts index f65b7cd0aa0b0..df2807e59ab46 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts @@ -20,10 +20,13 @@ // Field types export const FIELD_TYPES = { TEXT: 'text', + TEXTAREA: 'textarea', NUMBER: 'number', TOGGLE: 'toggle', CHECKBOX: 'checkbox', COMBO_BOX: 'comboBox', + RADIO_GROUP: 'radioGroup', + RANGE: 'range', SELECT: 'select', MULTI_SELECT: 'multiSelect', }; diff --git a/src/plugins/es_ui_shared/static/forms/components/helpers.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts similarity index 96% rename from src/plugins/es_ui_shared/static/forms/components/helpers.ts rename to src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts index a7543d31bb547..e71d52d6ff003 100644 --- a/src/plugins/es_ui_shared/static/forms/components/helpers.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts @@ -17,7 +17,7 @@ * under the License. */ -import { FieldHook } from '../hook_form_lib'; +import { FieldHook } from './types'; export const getFieldValidityAndErrorMessage = ( field: FieldHook diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 8a1012404b377..d7ef798bf2e03 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -27,6 +27,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) type = FIELD_TYPES.TEXT, defaultValue = '', label = '', + labelAppend = '', helpText = '', validations = [], formatters = [], @@ -382,6 +383,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) path, type, label, + labelAppend, helpText, value, errors, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts index 6e1a5b075d318..3079814c9ad14 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts @@ -20,6 +20,7 @@ // Only export the useForm hook. The "useField" hook is for internal use // as the consumer of the library must use the component export { useForm } from './hooks'; +export { getFieldValidityAndErrorMessage } from './helpers'; export * from './form_context'; export * from './components'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 28e2a346bd5c4..9946020132354 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -71,6 +71,7 @@ export interface FormOptions { export interface FieldHook { readonly path: string; readonly label?: string; + readonly labelAppend?: string | ReactNode; readonly helpText?: string | ReactNode; readonly type: string; readonly value: unknown; @@ -98,6 +99,7 @@ export interface FieldHook { export interface FieldConfig { readonly path?: string; readonly label?: string; + readonly labelAppend?: string | ReactNode; readonly helpText?: string | ReactNode; readonly type?: HTMLInputElement['type']; readonly defaultValue?: unknown; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx index 168cacf3e97e1..53365a4daa34a 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx @@ -35,7 +35,7 @@ FlexItem.displayName = 'FlexItem'; interface HeaderGlobalProps { hideDetectionEngine?: boolean; } -export const HeaderGlobal = React.memo(({ hideDetectionEngine = true }) => ( +export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx index 4db2a35c600e9..9877372ff9f41 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBetaBadge, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { + EuiBetaBadge, + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiTitle, +} from '@elastic/eui'; import React from 'react'; import styled, { css } from 'styled-components'; @@ -14,6 +21,7 @@ import { Subtitle, SubtitleProps } from '../subtitle'; interface HeaderProps { border?: boolean; + isLoading?: boolean; } const Header = styled.header.attrs({ @@ -26,6 +34,9 @@ const Header = styled.header.attrs({ css` border-bottom: ${theme.eui.euiBorderThin}; padding-bottom: ${theme.eui.paddingSizes.l}; + .euiProgress { + top: ${theme.eui.paddingSizes.l}; + } `} `} `; @@ -85,6 +96,7 @@ export const HeaderPage = React.memo( border, children, draggableArguments, + isLoading, subtitle, subtitle2, title, @@ -132,6 +144,7 @@ export const HeaderPage = React.memo( {subtitle && } {subtitle2 && } + {border && isLoading && } {children && ( diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx index d619b515ccc7a..ce102d7ade53b 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -84,6 +84,7 @@ describe('QueryBar ', () => { } = wrapper.find(SearchBar).props(); expect(searchBarProps).toEqual({ + dataTestSubj: undefined, dateRangeFrom: 'now-24h', dateRangeTo: 'now', filters: [], @@ -178,6 +179,7 @@ describe('QueryBar ', () => { title: 'filebeat-*,auditbeat-*,packetbeat-*', }, ], + isLoading: false, isRefreshPaused: true, query: { language: 'kuery', diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx index 8b5f3b0f4d425..3f460560b79b5 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -25,6 +25,7 @@ export interface QueryBarComponentProps { dateRangeTo?: string; hideSavedQuery?: boolean; indexPattern: StaticIndexPattern; + isLoading?: boolean; isRefreshPaused?: boolean; filterQuery: Query; filterManager: FilterManager; @@ -42,6 +43,7 @@ export const QueryBar = memo( dateRangeTo, hideSavedQuery = false, indexPattern, + isLoading = false, isRefreshPaused, filterQuery, filterManager, @@ -125,6 +127,7 @@ export const QueryBar = memo( dateRangeTo={dateRangeTo} filters={filters} indexPatterns={indexPatterns} + isLoading={isLoading} isRefreshPaused={isRefreshPaused} query={draftQuery} onClearSavedQuery={onClearSavedQuery} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 333baefe034fd..798cf91612a85 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -6,15 +6,40 @@ import chrome from 'ui/chrome'; import { + AddRulesProps, DeleteRulesProps, DuplicateRulesProps, EnableRulesProps, FetchRulesProps, FetchRulesResponse, + NewRule, Rule, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; +/** + * Add provided Rule + * + * @param rule to add + * @param kbnVersion current Kibana Version to use for headers + */ +export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Promise => { + const response = await fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: JSON.stringify(rule), + signal, + }); + + await throwIfNotOk(response); + return response.json(); +}; + /** * Fetches all rules or single specified rule from the Detection Engine API * @@ -55,12 +80,6 @@ export const fetchRules = async ({ const response = await fetch(endpoint, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-version': kbnVersion, - 'kbn-xsrf': kbnVersion, - }, signal, }); await throwIfNotOk(response); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx new file mode 100644 index 0000000000000..dbc148a96365d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty, get } from 'lodash/fp'; +import { useEffect, useState, Dispatch, SetStateAction } from 'react'; +import { StaticIndexPattern } from 'ui/index_patterns'; + +import { getIndexFields, sourceQuery } from '../../../containers/source'; +import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { SourceQuery } from '../../../graphql/types'; +import { useApolloClient } from '../../../utils/apollo_context'; + +import * as i18n from './translations'; + +interface FetchIndexPattern { + isLoading: boolean; + indices: string[]; + indicesExists: boolean; + indexPatterns: StaticIndexPattern | null; +} + +type Return = [FetchIndexPattern, Dispatch>]; + +export const useFetchIndexPatterns = (): Return => { + const apolloClient = useApolloClient(); + const [indices, setIndices] = useState([]); + const [indicesExists, setIndicesExists] = useState(false); + const [indexPatterns, setIndexPatterns] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + async function fetchIndexPatterns() { + if (apolloClient && !isEmpty(indices)) { + setIsLoading(true); + apolloClient + .query({ + query: sourceQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId: 'default', + defaultIndex: indices, + }, + context: { + fetchOptions: { + signal: abortCtrl.signal, + }, + }, + }) + .then( + result => { + if (isSubscribed) { + setIsLoading(false); + setIndicesExists(get('data.source.status.indicesExist', result)); + setIndexPatterns( + getIndexFields(indices.join(), get('data.source.status.indexFields', result)) + ); + } + }, + error => { + if (isSubscribed) { + setIsLoading(false); + errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); + } + } + ); + } + } + fetchIndexPatterns(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [indices]); + + return [{ isLoading, indices, indicesExists, indexPatterns }, setIndices]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx new file mode 100644 index 0000000000000..371d28aebf7f7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState, Dispatch } from 'react'; + +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; + +import { addRule as persistRule } from './api'; +import * as i18n from './translations'; +import { NewRule } from './types'; + +interface PersistRuleReturn { + isLoading: boolean; + isSaved: boolean; +} + +type Return = [PersistRuleReturn, Dispatch]; + +export const usePersistRule = (): Return => { + const [rule, setRule] = useState(null); + const [isSaved, setIsSaved] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setIsSaved(false); + async function saveRule() { + if (rule != null) { + try { + setIsLoading(true); + await persistRule({ rule, kbnVersion, signal: abortCtrl.signal }); + + if (isSubscribed) { + setIsSaved(true); + } + } catch (error) { + if (isSubscribed) { + errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setIsLoading(false); + } + } + } + + saveRule(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [rule]); + + return [{ isLoading, isSaved }, setRule]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts index a1ea2afb822f9..39efbde2ad5c2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts @@ -9,3 +9,10 @@ import { i18n } from '@kbn/i18n'; export const RULE_FETCH_FAILURE = i18n.translate('xpack.siem.containers.detectionEngine.rules', { defaultMessage: 'Failed to fetch Rules', }); + +export const RULE_ADD_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.addRuleFailDescription', + { + defaultMessage: 'Failed to add Rule', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index afb0158fea677..fe6fb04800adc 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -6,6 +6,41 @@ import * as t from 'io-ts'; +export const NewRuleSchema = t.intersection([ + t.type({ + description: t.string, + enabled: t.boolean, + index: t.array(t.string), + interval: t.string, + language: t.string, + name: t.string, + query: t.string, + severity: t.string, + type: t.union([t.literal('query'), t.literal('saved_query')]), + }), + t.partial({ + created_by: t.string, + false_positives: t.array(t.string), + from: t.string, + id: t.string, + max_signals: t.number, + references: t.array(t.string), + rule_id: t.string, + tags: t.array(t.string), + to: t.string, + updated_by: t.string, + }), +]); + +export const NewRulesSchema = t.array(NewRuleSchema); +export type NewRule = t.TypeOf; + +export interface AddRulesProps { + rule: NewRule; + kbnVersion: string; + signal: AbortSignal; +} + export const RuleSchema = t.intersection([ t.type({ created_by: t.string, diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index ff6e5e4d0c788..bc7b87cda6af9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -7,7 +7,7 @@ import { isUndefined } from 'lodash'; import { get, keyBy, pick, set } from 'lodash/fp'; import { Query } from 'react-apollo'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import memoizeOne from 'memoize-one'; import { StaticIndexPattern } from 'ui/index_patterns'; import chrome from 'ui/chrome'; @@ -16,6 +16,9 @@ import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { IndexField, SourceQuery } from '../../graphql/types'; import { sourceQuery } from './index.gql_query'; +import { useApolloClient } from '../../utils/apollo_context'; + +export { sourceQuery }; export interface BrowserField { aggregatable: boolean; @@ -57,7 +60,7 @@ interface WithSourceProps { sourceId: string; } -const getIndexFields = memoizeOne( +export const getIndexFields = memoizeOne( (title: string, fields: IndexField[]): StaticIndexPattern => fields && fields.length > 0 ? { @@ -110,3 +113,56 @@ WithSource.displayName = 'WithSource'; export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => indicesExist || isUndefined(indicesExist); + +export const useWithSource = (sourceId: string, indices: string[]) => { + const [loading, updateLoading] = useState(false); + const [indicesExist, setIndicesExist] = useState(undefined); + const [browserFields, setBrowserFields] = useState(null); + const [indexPattern, setIndexPattern] = useState(null); + const [errorMessage, updateErrorMessage] = useState(null); + + const apolloClient = useApolloClient(); + async function fetchSource(signal: AbortSignal) { + updateLoading(true); + if (apolloClient) { + apolloClient + .query({ + query: sourceQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + defaultIndex: indices, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateErrorMessage(null); + setIndicesExist(get('data.source.status.indicesExist', result)); + setBrowserFields(getBrowserFields(get('data.source.status.indexFields', result))); + setIndexPattern( + getIndexFields(indices.join(), get('data.source.status.indexFields', result)) + ); + }, + error => { + updateLoading(false); + updateErrorMessage(error.message); + } + ); + } + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchSource(signal); + return () => abortCtrl.abort(); + }, [apolloClient, sourceId, indices]); + + return { indicesExist, browserFields, indexPattern, loading, errorMessage }; +}; diff --git a/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx b/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx new file mode 100644 index 0000000000000..b1defcb34066d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; + +import { DEFAULT_DARK_MODE } from '../../../common/constants'; +import { useKibanaUiSetting } from '../settings/use_kibana_ui_setting'; + +export const useEuiTheme = () => { + const [darkMode] = useKibanaUiSetting(DEFAULT_DARK_MODE); + return darkMode ? darkTheme : lightTheme; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx new file mode 100644 index 0000000000000..66353a9613650 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import React, { memo } from 'react'; + +import { RuleStatusIcon, RuleStatusIconProps } from '../status_icon'; + +interface AccordionTitleProps extends RuleStatusIconProps { + title: string; +} + +export const AccordionTitle = memo(({ name, title, type }) => ( + + + + + + +
    {title}
    +
    +
    +
    +)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx new file mode 100644 index 0000000000000..6673262a15906 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; +import { isEmpty, isEqual } from 'lodash/fp'; +import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import * as I18n from './translations'; + +interface AddItemProps { + addText: string; + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled: boolean; +} + +export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [items, setItems] = useState(['']); + const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(false); + + const lastInputRef = useRef(null); + + const removeItem = useCallback( + (index: number) => { + const values = field.value as string[]; + field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + }, + [field] + ); + + const addItem = useCallback(() => { + const values = field.value as string[]; + if (!isEmpty(values[values.length - 1])) { + field.setValue([...values, '']); + } + }, [field]); + + const updateItem = useCallback( + (event: ChangeEvent, index: number) => { + const values = field.value as string[]; + const value = event.target.value; + if (isEmpty(value)) { + setHaveBeenKeyboardDeleted(true); + field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + } else { + field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); + } + }, + [field] + ); + + const handleLastInputRef = useCallback( + (element: HTMLInputElement | null) => { + lastInputRef.current = element; + }, + [lastInputRef] + ); + + useEffect(() => { + if (!isEqual(field.value, items)) { + setItems( + isEmpty(field.value) + ? [''] + : haveBeenKeyboardDeleted + ? [...(field.value as string[]), ''] + : (field.value as string[]) + ); + setHaveBeenKeyboardDeleted(false); + } + }, [field.value]); + + useEffect(() => { + if (!haveBeenKeyboardDeleted && lastInputRef != null && lastInputRef.current != null) { + lastInputRef.current.focus(); + } + }, [haveBeenKeyboardDeleted, lastInputRef]); + + return ( + + <> + {items.map((item, index) => { + const euiFieldProps = { + disabled: isDisabled, + ...(index === items.length - 1 ? { inputRef: handleLastInputRef } : {}), + }; + return ( +
    + removeItem(index)} + aria-label={I18n.DELETE} + /> + } + value={item} + onChange={e => updateItem(e, index)} + compressed + fullWidth + {...euiFieldProps} + /> + {items.length - 1 !== index && } +
    + ); + })} + + + {addText} + + +
    + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts new file mode 100644 index 0000000000000..98c15606d88fe --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const DELETE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.addItem.deleteDescription', + { + defaultMessage: 'Delete', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx new file mode 100644 index 0000000000000..4e7832c890255 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow } from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { useCallback, useEffect, useState } from 'react'; +import { StaticIndexPattern } from 'ui/index_patterns'; +import { Subscription } from 'rxjs'; +import styled from 'styled-components'; + +import { SavedQueryTimeFilter } from '../../../../../../../../../../src/legacy/core_plugins/data/public/search'; +import { SavedQuery } from '../../../../../../../../../../src/legacy/core_plugins/data/public'; +import { + esFilters, + Query, + FilterManager, +} from '../../../../../../../../../../src/plugins/data/public'; + +import { QueryBar } from '../../../../../components/query_bar'; +import { useKibanaCore } from '../../../../../lib/compose/kibana_core'; +import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; + +export interface FieldValueQueryBar { + filters: esFilters.Filter[]; + query: Query; + saved_id: string; +} +interface QueryBarDefineRuleProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + isLoading: boolean; + indexPattern: StaticIndexPattern; +} + +const StyledEuiFormRow = styled(EuiFormRow)` + .kbnTypeahead__items { + max-height: 14vh !important; + } + .globalQueryBar { + padding: 4px 0px 0px 0px; + .kbnQueryBar { + & > div:first-child { + margin: 0px 0px 0px 4px; + } + } + } +`; + +// TODO need to add disabled in the SearchBar + +export const QueryBarDefineRule = ({ + dataTestSubj, + field, + idAria, + indexPattern, + isLoading = false, +}: QueryBarDefineRuleProps) => { + const [savedQuery, setSavedQuery] = useState(null); + const [queryDraft, setQueryDraft] = useState({ query: '', language: 'kuery' }); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const core = useKibanaCore(); + const [filterManager] = useState(new FilterManager(core.uiSettings)); + + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters([]); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const newFilters = filterManager.getFilters(); + const { filters } = field.value as FieldValueQueryBar; + + if (!isEqual(filters, newFilters)) { + field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); + } + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, [field.value]); + + useEffect(() => { + let isSubscribed = true; + async function updateFilterQueryFromValue() { + const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; + if (!isEqual(query, queryDraft)) { + setQueryDraft(query); + } + if (!isEqual(filters, filterManager.getFilters())) { + filterManager.setFilters(filters); + } + if ( + (savedId != null && savedQuery != null && savedId !== savedQuery.id) || + (savedId != null && savedQuery == null) + ) { + try { + const mySavedQuery = await savedQueryServices.getSavedQuery(savedId); + if (isSubscribed && mySavedQuery != null) { + setSavedQuery(mySavedQuery); + } + } catch { + setSavedQuery(null); + } + } else if (savedId == null && savedQuery != null) { + setSavedQuery(null); + } + } + updateFilterQueryFromValue(); + return () => { + isSubscribed = false; + }; + }, [field.value]); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + const { query } = field.value as FieldValueQueryBar; + if (!isEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + const { query } = field.value as FieldValueQueryBar; + if (!isEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + const { saved_id: savedId } = field.value as FieldValueQueryBar; + if (newSavedQuery.id !== savedId) { + setSavedQuery(newSavedQuery); + field.setValue({ + filters: newSavedQuery.attributes.filters, + query: newSavedQuery.attributes.query, + saved_id: newSavedQuery.id, + }); + } + } + }, + [field.value] + ); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx new file mode 100644 index 0000000000000..ebb365f6087a9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; + +import * as I18n from './translations'; + +interface ScheduleItemProps { + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled: boolean; +} + +const timeTypeOptions = [ + { value: 's', text: I18n.SECONDS }, + { value: 'm', text: I18n.MINUTES }, + { value: 'h', text: I18n.HOURS }, +]; + +const StyledEuiFormRow = styled(EuiFormRow)` + .euiFormControlLayout { + max-width: 200px !important; + } +`; + +export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: ScheduleItemProps) => { + const [timeType, setTimeType] = useState('s'); + const [timeVal, setTimeVal] = useState(0); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const onChangeTimeType = useCallback(e => { + setTimeType(e.target.value); + }, []); + + const onChangeTimeVal = useCallback(e => { + const sanitizedValue: number = parseInt(e.target.value, 10); + setTimeVal(isNaN(sanitizedValue) ? 0 : sanitizedValue); + }, []); + + useEffect(() => { + if (!isEmpty(timeVal) && Number(timeVal) >= 0 && field.value !== `${timeVal}${timeType}`) { + field.setValue(`${timeVal}${timeType}`); + } + }, [field.value, timeType, timeVal]); + + useEffect(() => { + if (!isEmpty(field.value)) { + const filterTimeVal = (field.value as string).match(/\d+/g); + const filterTimeType = (field.value as string).match(/[a-zA-Z]+/g); + if ( + !isEmpty(filterTimeVal) && + filterTimeVal != null && + !isNaN(Number(filterTimeVal[0])) && + Number(filterTimeVal[0]) !== Number(timeVal) + ) { + setTimeVal(Number(filterTimeVal[0])); + } + if ( + !isEmpty(filterTimeType) && + filterTimeType != null && + ['s', 'm', 'h'].includes(filterTimeType[0]) && + filterTimeType[0] !== timeType + ) { + setTimeType(filterTimeType[0]); + } + } + }, [field.value]); + + // EUI missing some props + const rest = { disabled: isDisabled }; + + return ( + + + } + compressed + fullWidth + min={0} + onChange={onChangeTimeVal} + value={timeVal} + {...rest} + /> + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts new file mode 100644 index 0000000000000..1bc983814c330 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SECONDS = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRuleForm.secondsOptionDescription', + { + defaultMessage: 'Seconds', + } +); + +export const MINUTES = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRuleForm.minutesOptionDescription', + { + defaultMessage: 'Minutes', + } +); + +export const HOURS = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRuleForm.hoursOptionDescription', + { + defaultMessage: 'Hours', + } +); + +export const INVALID_TIME = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRuleForm.invalidTimeMessageDescription', + { + defaultMessage: 'A time is required.', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts new file mode 100644 index 0000000000000..6c91c4a02edf9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + getUseField, + getFieldValidityAndErrorMessage, + FieldHook, + Form, + FormDataProvider, + UseField, + useForm, +} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx new file mode 100644 index 0000000000000..ad0011ff8ed18 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiAvatar, EuiIcon } from '@elastic/eui'; +import React, { memo } from 'react'; +import styled from 'styled-components'; + +import { useEuiTheme } from '../../../../../lib/theme/use_eui_theme'; + +export type RuleStatusType = 'passive' | 'active' | 'valid'; + +export interface RuleStatusIconProps { + name: string; + type: RuleStatusType; +} + +const RuleStatusIconStyled = styled.div` + position: relative; + svg { + position: absolute; + top: 8px; + left: 9px; + } +`; + +export const RuleStatusIcon = memo(({ name, type }) => { + const theme = useEuiTheme(); + const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorDarkestShade; + return ( + + + {type === 'valid' ? : null} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts new file mode 100644 index 0000000000000..7d6e434bcc8c6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as I18n from './translations'; + +export type SeverityValue = 'low' | 'medium' | 'high' | 'critical'; + +interface SeverityOptionItem { + value: SeverityValue; + text: string; +} + +export const severityOptions: SeverityOptionItem[] = [ + { value: 'low', text: I18n.LOW }, + { value: 'medium', text: I18n.MEDIUM }, + { value: 'high', text: I18n.HIGH }, + { value: 'critical', text: I18n.CRITICAL }, +]; + +export const defaultRiskScoreBySeverity: Record = { + low: 21, + medium: 47, + high: 73, + critical: 99, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts new file mode 100644 index 0000000000000..b94fa8c933937 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const defaultValue = { + name: '', + description: '', + severity: 'low', + riskScore: 50, + references: [], + falsePositives: [], + tags: [], +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx new file mode 100644 index 0000000000000..4393f39ad2f85 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; + +import { RuleStepProps, RuleStep } from '../../types'; +import * as CreateRuleI18n from '../../translations'; +import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { AddItem } from '../add_item_form'; +import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; +import { defaultValue } from './default_value'; +import { schema } from './schema'; +import * as I18n from './translations'; + +const CommonUseField = getUseField({ component: Field }); + +export const StepAboutRule = memo(({ isLoading, setStepData }) => { + const { form } = useForm({ + schema, + defaultValue, + options: { stripEmptyFields: false }, + }); + + const onSubmit = useCallback(async () => { + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.aboutRule, data, newIsValid); + } + }, [form]); + + return ( + <> +
    + + + + + + + + + {({ severity }) => { + const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; + const riskScoreField = form.getFields().riskScore; + if (newRiskScore != null && riskScoreField.value !== newRiskScore) { + riskScoreField.setValue(newRiskScore); + } + return null; + }} + + + + + + + {CreateRuleI18n.CONTINUE} + + + + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx new file mode 100644 index 0000000000000..97ad3d595a938 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FormSchema, + FIELD_TYPES, +} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +import * as CreateRuleI18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldNameLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError', + { + defaultMessage: 'A name is required.', + } + ) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldDescriptionLabel', + { + defaultMessage: 'Description', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } + ) + ), + }, + ], + }, + severity: { + type: FIELD_TYPES.SELECT, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel', + { + defaultMessage: 'Severity', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', + { + defaultMessage: 'A severity is required.', + } + ) + ), + }, + ], + }, + riskScore: { + type: FIELD_TYPES.RANGE, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldRiskScoreLabel', + { + defaultMessage: 'Risk score', + } + ), + }, + references: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel', + { + defaultMessage: 'Reference URLs', + } + ), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + }, + falsePositives: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel', + { + defaultMessage: 'False positives', + } + ), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + }, + tags: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', { + defaultMessage: 'Tags', + }), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts new file mode 100644 index 0000000000000..bd759b345d70d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ADD_REFERENCE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription', + { + defaultMessage: 'Add reference', + } +); + +export const ADD_FALSE_POSITIVE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.addFalsePositiveDescription', + { + defaultMessage: 'Add false positive', + } +); + +export const LOW = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription', + { + defaultMessage: 'Low', + } +); + +export const MEDIUM = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.severityOptionMediumDescription', + { + defaultMessage: 'Medium', + } +); + +export const HIGH = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription', + { + defaultMessage: 'High', + } +); + +export const CRITICAL = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRuleForm.severityOptionCriticalDescription', + { + defaultMessage: 'Critical', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx new file mode 100644 index 0000000000000..b09d0df962793 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { memo, useCallback, useEffect, useState } from 'react'; + +import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules/fetch_index_patterns'; +import { DEFAULT_INDEX_KEY, DEFAULT_SIGNALS_INDEX_KEY } from '../../../../../../common/constants'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import * as CreateRuleI18n from '../../translations'; +import { RuleStep, RuleStepProps } from '../../types'; +import { QueryBarDefineRule } from '../query_bar'; +import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { schema } from './schema'; +import * as I18n from './translations'; + +const CommonUseField = getUseField({ component: Field }); + +export const StepDefineRule = memo(({ isLoading, setStepData }) => { + const [initializeOutputIndex, setInitializeOutputIndex] = useState(true); + const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); + const [ + { indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, + setIndices, + ] = useFetchIndexPatterns(); + const [indicesConfig] = useKibanaUiSetting(DEFAULT_INDEX_KEY); + const [signalIndexConfig] = useKibanaUiSetting(DEFAULT_SIGNALS_INDEX_KEY); + + const { form } = useForm({ + schema, + defaultValue: { + index: indicesConfig || [], + outputIndex: signalIndexConfig, + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: null, + }, + useIndicesConfig: 'true', + }, + options: { stripEmptyFields: false }, + }); + + const onSubmit = useCallback(async () => { + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.defineRule, data, newIsValid); + } + }, [form]); + + useEffect(() => { + if (signalIndexConfig != null && initializeOutputIndex) { + const outputIndexField = form.getFields().outputIndex; + outputIndexField.setValue(signalIndexConfig); + setInitializeOutputIndex(false); + } + }, [initializeOutputIndex, signalIndexConfig, form]); + + return ( + <> +
    + + + + + + {({ useIndicesConfig }) => { + if (localUseIndicesConfig !== useIndicesConfig) { + const indexField = form.getFields().index; + if ( + indexField != null && + useIndicesConfig === 'true' && + !isEqual(indexField.value, indicesConfig) + ) { + indexField.setValue(indicesConfig); + setIndices(indicesConfig); + } else if ( + indexField != null && + useIndicesConfig === 'false' && + !isEqual(indexField.value, []) + ) { + indexField.setValue([]); + setIndices([]); + } + setLocalUseIndicesConfig(useIndicesConfig); + } + + return null; + }} + + + + + + + {CreateRuleI18n.CONTINUE} + + + + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx new file mode 100644 index 0000000000000..500557a2c2a96 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { fromKueryExpression } from '@kbn/es-query'; +import { isEmpty } from 'lodash/fp'; +import React from 'react'; + +import { + FormSchema, + FIELD_TYPES, + ValidationFunc, +} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +import { ERROR_CODE } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; + +import * as CreateRuleI18n from '../../translations'; + +import { FieldValueQueryBar } from '../query_bar'; +import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY } from './translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + outputIndex: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldOutputIndiceNameLabel', + { + defaultMessage: 'Output index name', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'An output indice name for signals is required.', + } + ) + ), + }, + ], + }, + useIndicesConfig: { + type: FIELD_TYPES.RADIO_GROUP, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldIndicesTypeLabel', + { + defaultMessage: 'Indices type', + } + ), + }, + index: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndicesLabel', { + defaultMessage: 'Indices', + }), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'An output indice name for signals is required.', + } + ) + ), + }, + ], + }, + queryBar: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', + { + defaultMessage: 'Custom query', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + const { query, filters } = value as FieldValueQueryBar; + return isEmpty(query.query as string) && isEmpty(filters) + ? { + code: 'ERR_FIELD_MISSING', + path, + message: CUSTOM_QUERY_REQUIRED, + } + : undefined; + }, + }, + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + const { query } = value as FieldValueQueryBar; + if (!isEmpty(query.query as string) && query.language === 'kuery') { + try { + fromKueryExpression(query.query); + } catch (err) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: INVALID_CUSTOM_QUERY, + }; + } + } + return undefined; + }, + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx new file mode 100644 index 0000000000000..0050c59a4a2c8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const CUSTOM_QUERY_REQUIRED = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError', + { + defaultMessage: 'A custom query is required.', + } +); + +export const INVALID_CUSTOM_QUERY = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError', + { + defaultMessage: 'The KQL is invalid', + } +); + +export const CONFIG_INDICES = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.indicesFromConfigDescription', + { + defaultMessage: 'Use Elasticsearch indices from SIEM advanced settings', + } +); + +export const CUSTOM_INDICES = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.indicesCustomDescription', + { + defaultMessage: 'Provide custom list of indices', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts new file mode 100644 index 0000000000000..df52b0c9ff64e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FieldValueQueryBar } from '../query_bar'; + +export interface QueryBarStepDefineRule { + queryBar: FieldValueQueryBar; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx new file mode 100644 index 0000000000000..10b95ac6c8742 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; + +import { RuleStep, RuleStepProps } from '../../types'; +import { ScheduleItem } from '../schedule_item_form'; +import { Form, UseField, useForm } from '../shared_imports'; +import { schema } from './schema'; +import * as I18n from './translations'; + +export const StepScheduleRule = memo(({ isLoading, setStepData }) => { + const { form } = useForm({ + schema, + defaultValue: { + interval: '5m', + from: '0m', + }, + options: { stripEmptyFields: false }, + }); + + const onSubmit = useCallback( + async (enabled: boolean) => { + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid); + } + }, + [form] + ); + + return ( + <> +
    + + + + + + + + {I18n.COMPLETE_WITHOUT_ACTIVATING} + + + + + {I18n.COMPLETE_WITH_ACTIVATING} + + + + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx new file mode 100644 index 0000000000000..6192a3b905879 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FormSchema } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +import * as CreateRuleI18n from '../../translations'; + +export const schema: FormSchema = { + interval: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel', + { + defaultMessage: 'Rule run interval & look-back', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText', + { + defaultMessage: 'How often and how far back this rule will search specified indices.', + } + ), + }, + from: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel', + { + defaultMessage: 'Additional look-back', + } + ), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', + { + defaultMessage: + 'Add more time to the look-back range in order to prevent potential gaps in signal reporting.', + } + ), + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx new file mode 100644 index 0000000000000..feaaf4e85b2af --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate( + 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle', + { + defaultMessage: 'Complete rule without activating', + } +); + +export const COMPLETE_WITH_ACTIVATING = i18n.translate( + 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle', + { + defaultMessage: 'Complete rule & activate', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts new file mode 100644 index 0000000000000..b864260dd3338 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import moment from 'moment'; + +import { NewRule } from '../../../containers/detection_engine/rules/types'; + +import { + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + DefineStepRuleJson, + ScheduleStepRuleJson, + AboutStepRuleJson, + FormatRuleType, +} from './types'; + +const getTimeTypeValue = (time: string): { unit: string; value: number } => { + const timeObj = { + unit: '', + value: 0, + }; + const filterTimeVal = (time as string).match(/\d+/g); + const filterTimeType = (time as string).match(/[a-zA-Z]+/g); + if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { + timeObj.value = Number(filterTimeVal[0]); + } + if ( + !isEmpty(filterTimeType) && + filterTimeType != null && + ['s', 'm', 'h'].includes(filterTimeType[0]) + ) { + timeObj.unit = filterTimeType[0]; + } + return timeObj; +}; + +const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { + const { queryBar, useIndicesConfig, outputIndex, ...rest } = defineStepData; + const { filters, query, saved_id: savedId } = queryBar; + return { + ...rest, + language: query.language, + filters, + output_index: outputIndex, + query: query.query as string, + ...(savedId != null ? { saved_id: savedId } : {}), + }; +}; + +const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { + const formatScheduleData = scheduleData; + + if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { + const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( + formatScheduleData.interval + ); + const { unit: fromUnit, value: fromValue } = getTimeTypeValue(formatScheduleData.from); + const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); + duration.add(fromValue, fromUnit as 's' | 'm' | 'h'); + formatScheduleData.from = `now-${duration.asSeconds()}s`; + formatScheduleData.to = 'now'; + } + return formatScheduleData; +}; + +const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { + const { falsePositives, references, riskScore, ...rest } = aboutStepData; + + return { + false_positives: falsePositives.filter(item => !isEmpty(item)), + references: references.filter(item => !isEmpty(item)), + risk_score: riskScore, + ...rest, + }; +}; + +export const formatRule = ( + defineStepData: DefineStepRule, + aboutStepData: AboutStepRule, + scheduleData: ScheduleStepRule +): NewRule => { + const type: FormatRuleType = defineStepData.queryBar.saved_id != null ? 'saved_query' : 'query'; + const persistData = { + type, + ...formatDefineStepData(defineStepData), + ...formatAboutStepData(aboutStepData), + ...formatScheduleStepData(scheduleData), + meta: { + from: scheduleData.from, + }, + }; + + return persistData; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx index 47a3527aff99c..c505124c25039 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx @@ -4,22 +4,189 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import { EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useRef, useState } from 'react'; +import { Redirect } from 'react-router-dom'; import { HeaderPage } from '../../../components/header_page'; import { WrapperPage } from '../../../components/wrapper_page'; +import { AccordionTitle } from './components/accordion_title'; +import { StepAboutRule } from './components/step_about_rule'; +import { StepDefineRule } from './components/step_define_rule'; +import { StepScheduleRule } from './components/step_schedule_rule'; +import { usePersistRule } from '../../../containers/detection_engine/rules/persist_rule'; import { SpyRoute } from '../../../utils/route/spy_routes'; + +import { formatRule } from './helpers'; import * as i18n from './translations'; +import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from './types'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine'; + +const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule]; export const CreateRuleComponent = React.memo(() => { + const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); + const defineRuleRef = useRef(null); + const aboutRuleRef = useRef(null); + const scheduleRuleRef = useRef(null); + const stepsData = useRef>({ + [RuleStep.defineRule]: { isValid: false, data: {} }, + [RuleStep.aboutRule]: { isValid: false, data: {} }, + [RuleStep.scheduleRule]: { isValid: false, data: {} }, + }); + const [{ isLoading, isSaved }, setRule] = usePersistRule(); + + const setStepData = (step: RuleStep, data: unknown, isValid: boolean) => { + stepsData.current[step] = { data, isValid }; + if (isValid) { + const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); + if ([0, 1].includes(stepRuleIdx)) { + openCloseAccordion(step); + openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + } else if ( + stepRuleIdx === 2 && + stepsData.current[RuleStep.defineRule].isValid && + stepsData.current[RuleStep.aboutRule].isValid + ) { + setRule( + formatRule( + stepsData.current[RuleStep.defineRule].data as DefineStepRule, + stepsData.current[RuleStep.aboutRule].data as AboutStepRule, + stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule + ) + ); + } + } + }; + + const getAccordionType = useCallback( + (accordionId: RuleStep) => { + if (accordionId === openAccordionId) { + return 'active'; + } else if (stepsData.current[accordionId].isValid) { + return 'valid'; + } + return 'passive'; + }, + [openAccordionId, stepsData.current] + ); + + const defineRuleButton = ( + + ); + + const aboutRuleButton = ( + + ); + + const scheduleRuleButton = ( + + ); + + const openCloseAccordion = (accordionId: RuleStep | null) => { + if (accordionId != null) { + if (accordionId === RuleStep.defineRule && defineRuleRef.current != null) { + defineRuleRef.current.onToggle(); + } else if (accordionId === RuleStep.aboutRule && aboutRuleRef.current != null) { + aboutRuleRef.current.onToggle(); + } else if (accordionId === RuleStep.scheduleRule && scheduleRuleRef.current != null) { + scheduleRuleRef.current.onToggle(); + } + } + }; + + const manageAccordions = useCallback( + (id: RuleStep, isOpen: boolean) => { + const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id); + const isLatestStepsRuleValid = + stepRuleIdx === 0 + ? true + : stepsRuleOrder + .filter((stepRule, index) => index < stepRuleIdx) + .every(stepRule => stepsData.current[stepRule].isValid); + + if ( + openAccordionId != null && + openAccordionId !== id && + !stepsData.current[openAccordionId].isValid && + isOpen + ) { + openCloseAccordion(id); + } else if (!isLatestStepsRuleValid && isOpen) { + openCloseAccordion(id); + } else if (openAccordionId != null && id !== openAccordionId && isOpen) { + openCloseAccordion(openAccordionId); + setOpenAccordionId(id); + } else if (openAccordionId == null && isOpen) { + setOpenAccordionId(id); + } + }, + [openAccordionId] + ); + + if (isSaved && stepsData.current[RuleStep.scheduleRule].isValid) { + return ; + } + return ( <> + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts index 884f3f3741228..ca96566305a6b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts @@ -9,3 +9,32 @@ import { i18n } from '@kbn/i18n'; export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', { defaultMessage: 'Create new rule', }); + +export const DEFINE_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.defineRuleTitle', { + defaultMessage: 'Define Rule', +}); + +export const ABOUT_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.aboutRuleTitle', { + defaultMessage: 'About Rule', +}); + +export const SCHEDULE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.scheduleRuleTitle', + { + defaultMessage: 'Schedule Rule', + } +); + +export const OPTIONAL_FIELD = i18n.translate( + 'xpack.siem.detectionEngine.createRule.optionalFieldDescription', + { + defaultMessage: 'Optional', + } +); + +export const CONTINUE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.continueButtonTitle', + { + defaultMessage: 'Continue', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts new file mode 100644 index 0000000000000..a03f6a0b11bee --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FieldValueQueryBar } from './components/query_bar'; +import { esFilters } from '../../../../../../../../src/plugins/data/common'; + +export enum RuleStep { + defineRule = 'define-rule', + aboutRule = 'about-rule', + scheduleRule = 'schedule-rule', +} + +export interface RuleStepData { + isValid: boolean; + data: unknown; +} + +export interface RuleStepProps { + setStepData: (step: RuleStep, data: unknown, isValid: boolean) => void; + isLoading: boolean; +} + +export interface DefineStepRule { + outputIndex: string; + useIndicesConfig: string; + index: string[]; + queryBar: FieldValueQueryBar; +} + +export interface DefineStepRuleJson { + output_index: string; + index: string[]; + filters: esFilters.Filter[]; + saved_id?: string; + query: string; + language: string; +} + +export interface AboutStepRule { + name: string; + description: string; + severity: string; + riskScore: number; + references: string[]; + falsePositives: string[]; + tags: string[]; +} + +export interface AboutStepRuleJson { + name: string; + description: string; + severity: string; + risk_score: number; + references: string[]; + false_positives: string[]; + tags: string[]; +} + +export interface ScheduleStepRule { + enabled: boolean; + interval: string; + from: string; + to?: string; +} +export type ScheduleStepRuleJson = ScheduleStepRule; + +export type FormatRuleType = 'query' | 'saved_query'; From bcd0c09899ae83395ceb3ca8b1919c450db82d46 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Fri, 22 Nov 2019 16:05:09 -0700 Subject: [PATCH 035/128] [Canvas][Docs] Updates function reference (#50393) * Updates Canvas function reference doc * Added context examples * Addressed feedback * escapes curly braces * Updated location help text * Fixes doc build error --- .../canvas/canvas-function-reference.asciidoc | 870 ++++++++---------- 1 file changed, 370 insertions(+), 500 deletions(-) diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 07f3cf028dc0e..330cc63d10548 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -28,7 +28,7 @@ Returns `true` if all of the conditions are met. See also <>. *Expression syntax* [source,js] ---- -all {neq “foo”} {neq “bar”} {neq “fizz”} +all {neq "foo"} {neq "bar"} {neq "fizz"} all condition={gt 10} condition={lt 20} ---- @@ -49,7 +49,7 @@ filters } | render ---- -This sets the color of the metric text to `”red”` if the context passed into `metric` is greater than or equal to 0 and less than 0.8. Otherwise, the color is set to `"green"`. +This sets the color of the metric text to `"red"` if the context passed into `metric` is greater than or equal to 0 and less than 0.8. Otherwise, the color is set to `"green"`. *Accepts:* `null` @@ -76,8 +76,8 @@ Converts between core types, including `string`, `number`, `null`, `boolean`, an *Expression syntax* [source,js] ---- -alterColumn “cost” type=”string” -alterColumn column=”@timestamp” name=”foo” +alterColumn "cost" type="string" +alterColumn column="@timestamp" name="foo" ---- *Code example* @@ -85,7 +85,7 @@ alterColumn column=”@timestamp” name=”foo” ---- filters | demodata -| alterColumn “time” name=”time_in_ms” type=”number” +| alterColumn "time" name="time_in_ms" type="number" | table | render ---- @@ -97,7 +97,7 @@ This renames the `time` column to `time_in_ms` and converts the type of the colu |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `column` |`string` @@ -124,7 +124,7 @@ Returns `true` if at least one of the conditions is met. See also <>. *Expression syntax* [source,js] ---- -any {eq “foo”} {eq “bar”} {eq “fizz”} +any {eq "foo"} {eq "bar"} {eq "fizz"} any condition={lte 10} condition={gt 30} ---- @@ -140,7 +140,7 @@ filters | pie | render ---- -This filters out any rows that don’t contain `“elasticsearch”`, `“kibana”` or `“x-pack”` in the `project` field. +This filters out any rows that don’t contain `"elasticsearch"`, `"kibana"` or `"x-pack"` in the `project` field. *Accepts:* `null` @@ -167,9 +167,9 @@ Creates a `datatable` with a single value. See also <>. *Expression syntax* [source,js] ---- -as -as “foo” -as name=”bar” +as +as "foo" +as name="bar" ---- *Code example* @@ -182,7 +182,7 @@ filters | plot | render ---- -`as` casts any primitive value (`string`, `number`, `date`, `null`) into a `datatable` with a single row and a single column with the given name (or defaults to `"value"` if no name is provided). This is useful when piping a primitive value into a function that only takes `datatable` as an input. +`as` casts any primitive value (`string`, `number`, `date`, `null`) into a `datatable` with a single row and a single column with the given name (or defaults to `"value"` if no name is provided). This is useful when piping a primitive value into a function that only takes `datatable` as an input. In the example above, `ply` expects each `fn` subexpression to return a `datatable` in order to merge the results of each `fn` back into a `datatable`, but using a `math` aggregation in the subexpressions returns a single `math` value, which is then cast into a `datatable` using `as`. @@ -192,7 +192,7 @@ In the example above, `ply` expects each `fn` subexpression to return a `datatab |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `name` |`string` @@ -223,7 +223,7 @@ asset id="asset-498f7429-4d56-42a2-a7e4-8bf08d98d114" image dataurl={asset "asset-c661a7cc-11be-45a1-a401-d7592ea7917a"} mode="contain" | render ---- -The image asset stored with the ID `“asset-c661a7cc-11be-45a1-a401-d7592ea7917a”` is passed into the `dataurl` argument of the `image` function to display the stored asset. +The image asset stored with the ID `"asset-c661a7cc-11be-45a1-a401-d7592ea7917a"` is passed into the `dataurl` argument of the `image` function to display the stored asset. *Accepts:* `null` @@ -251,7 +251,7 @@ Configures the axis of a visualization. Only used with <>. [source,js] ---- axisConfig show=false -axisConfig position=”right” min=0 max=10 tickSize=1 +axisConfig position="right" min=0 max=10 tickSize=1 ---- *Code example* @@ -260,9 +260,9 @@ axisConfig position=”right” min=0 max=10 tickSize=1 filters | demodata | pointseries x="size(cost)" y="project" color="project" -| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} - legend=false - xaxis={axisConfig position="top" min=0 max=400 tickSize=100} +| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} + legend=false + xaxis={axisConfig position="top" min=0 max=400 tickSize=100} yaxis={axisConfig position="right"} | render ---- @@ -276,21 +276,21 @@ This sets the `x-axis` to display on the top of the chart and sets the range of |`max` |`number`, `string`, `null` -|The maximum value displayed in the axis. Must be a `number`, a date in milliseconds since epoch, or an ISO8601 `string` +|The maximum value displayed in the axis. Must be a number, a date in milliseconds since epoch, or an ISO8601 string. |`min` |`number`, `string`, `null` -|The minimum value displayed in the axis. Must be a `number`, a date in milliseconds since epoch, or an ISO8601 `string` +|The minimum value displayed in the axis. Must be a number, a date in milliseconds since epoch, or an ISO8601 string. |`position` |`string` -|The position of the axis labels. For example, `"top"`, `"bottom"`, `"left"`, or `"right"`. +|The position of the axis labels. For example, `"top"`, `"bottom"`, `"left"`, or `"right"`. Default: `"left"` |`show` |`boolean` -|Show the axis labels? +|Show the axis labels? Default: `true` @@ -309,30 +309,30 @@ Default: `true` [[case_fn]] === `case` -Builds a `case` (including a condition/result) to pass to the <> function. +Builds a <>, including a condition and a result, to pass to the <> function. *Expression syntax* [source,js] ---- -case 0 then=”red” -case when=5 then=”yellow” -case if={lte 50} then=”green” +case 0 then="red" +case when=5 then="yellow" +case if={lte 50} then="green" ---- *Code example* [source,text] ---- math "random()" -| progress shape="gauge" label={formatnumber "0%"} - font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" +| progress shape="gauge" label={formatnumber "0%"} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" color={ - switch {case if={lte 0.5} then="green"} - {case if={all {gt 0.5} {lte 0.75}} then="orange"} + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} default="red" - }} + }} valueColor={ - switch {case if={lte 0.5} then="green"} - {case if={all {gt 0.5} {lte 0.75}} then="orange"} + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} default="red" } | render @@ -345,7 +345,7 @@ This sets the color of the progress indicator and the color of the label to `"gr |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `when` |`any` @@ -378,13 +378,13 @@ Clears the _context_, and returns `null`. [[columns_fn]] === `columns` -Includes or excludes columns from a data table. If you specify both, this will exclude first. +Includes or excludes columns from a `datatable`. When both arguments are specified, the excluded columns will be removed first. *Expression syntax* [source,js] ---- -columns include=”@timestamp, projects, cost” -columns exclude=”username, country, age” +columns include="@timestamp, projects, cost" +columns exclude="username, country, age" ---- *Code example* @@ -404,11 +404,11 @@ This only keeps the `price`, `cost`, `state`, and `project` columns from the `de |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `include` |`string` -|A comma-separated list of column names to keep in the `datatable`. +|A comma-separated list of column names to keep in the `datatable`. |`exclude` |`string` @@ -417,20 +417,18 @@ Alias: `include` *Returns:* `datatable` - + [float] [[compare_fn]] === `compare` -Compares the _context_ to specified value to determine `true` or `false`. -Usually used in combination with <> or <>. This only works with primitive types, -such as `number`, `string`, and `boolean`. See also <>, <>, <>, <>, <>, and <>. +Compares the _context_ to specified value to determine `true` or `false`. Usually used in combination with `<>` or <>. This only works with primitive types, such as `number`, `string`, `boolean`, `null`. See also <>, <>, <>, <>, <>, <> *Expression syntax* [source,js] ---- -compare “neq” to=”elasticsearch” -compare op=”lte” to=100 +compare "neq" to="elasticsearch" +compare op="lte" to=100 ---- *Code example* @@ -438,18 +436,18 @@ compare op=”lte” to=100 ---- filters | demodata -| mapColumn project - fn=${getCell project | - switch +| mapColumn project + fn={getCell project | + switch {case if={compare eq to=kibana} then=kibana} {case if={compare eq to=elasticsearch} then=elasticsearch} default="other" } | pointseries size="size(cost)" color="project" -| pie +| pie | render ---- -This maps all `project` values that aren’t `“kibana”` and `“elasticsearch”` to `“other”`. Alternatively, you can use the individual comparator functions instead of compare. See <>, <>, <>, <>, <>, and <>. +This maps all `project` values that aren’t `"kibana"` and `"elasticsearch"` to `"other"`. Alternatively, you can use the individual comparator functions instead of compare. *Accepts:* `string`, `number`, `boolean`, `null` @@ -457,17 +455,17 @@ This maps all `project` values that aren’t `“kibana”` and `“elasticsearc |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `op` |`string` -|The operator to use in the comparison: `eq` (equal to), `gt` (greater than), `gte` (greater than or equal to), `lt` (less than), `lte` (less than or equal to), `ne` or `neq` (not equal to). +|The operator to use in the comparison: `"eq"` (equal to), `"gt"` (greater than), `"gte"` (greater than or equal to), `"lt"` (less than), `"lte"` (less than or equal to), `"ne"` or `"neq"` (not equal to). Default: `"eq"` -|`to` +|`to` -Alias: `this`, `b` +Aliases: `b`, `this` |`any` |The value compared to the _context_. |=== @@ -484,29 +482,28 @@ Creates an object used for styling an element's container, including background, *Expression syntax* [source,js] ---- -containerStyle backgroundColor=”red”’ -containerStyle borderRadius=”50px” -containerStyle border=”1px solid black” -containerStyle padding=”5px” -containerStyle opacity=”0.5” -containerStyle overflow=”hidden” -containerStyle backgroundImage={asset id=asset-f40d2292-cf9e-4f2c-8c6f-a504a25e949c} - backgroundRepeat="no-repeat" - backgroundSize="cover" +containerStyle backgroundColor="red"’ +containerStyle borderRadius="50px" +containerStyle border="1px solid black" +containerStyle padding="5px" +containerStyle opacity="0.5" +containerStyle overflow="hidden" +containerStyle backgroundImage={asset id=asset-f40d2292-cf9e-4f2c-8c6f-a504a25e949c} + backgroundRepeat="no-repeat" + backgroundSize="cover" ---- *Code example* [source,text] ---- -shape "star" fill="#E61D35" maintainAspect=true -| render - containerStyle={ - containerStyle backgroundColor="#F8D546" - borderRadius="200px" - border="4px solid #05509F" - padding="0px" - opacity="0.9" - overflow="hidden" +shape "star" fill="#E61D35" maintainAspect=true +| render containerStyle={ + containerStyle backgroundColor="#F8D546" + borderRadius="200px" + border="4px solid #05509F" + padding="0px" + opacity="0.9" + overflow="hidden" } ---- @@ -566,8 +563,7 @@ Default: `"hidden"` [[context_fn]] === `context` -Returns whatever you pass into it. This can be useful when you need to use the -_context_ as an argument to a function as a sub-expression. +Returns whatever you pass into it. This can be useful when you need to use _context_ as argument to a function as a sub-expression. *Expression syntax* [source,js] @@ -580,40 +576,41 @@ context ---- date | formatdate "LLLL" -| markdown "Last updated: " {context} +| markdown "Last updated: " {context} | render ---- Using the `context` function allows us to pass the output, or _context_, of the previous function as a value to an argument in the next function. Here we get the formatted date string from the previous function and pass it as `content` for the markdown element. *Accepts:* `any` -*Returns:* Original _context_ +*Returns:* Depends on your input and arguments + [float] [[csv_fn]] -=== `csv` +=== `csv` Creates a `datatable` from CSV input. *Expression syntax* [source,js] ---- -csv “fruit, stock - kiwi, 10 - Banana, 5” +csv "fruit, stock + kiwi, 10 + Banana, 5" ---- *Code example* [source,text] ---- csv "fruit,stock - kiwi,10 - banana,5" + kiwi,10 + banana,5" | pointseries color=fruit size=stock | pie | render ---- -This is useful for quickly mocking data. +This creates a `datatable` with `fruit` and `stock` columns with two rows. This is useful for quickly mocking data. *Accepts:* `null` @@ -621,7 +618,7 @@ This is useful for quickly mocking data. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `data` |`string` @@ -631,14 +628,13 @@ Alias: `data` |`string` |The data separation character. -|`newLine` +|`newline` |`string` |The row separation character. |=== *Returns:* `datatable` - [float] [[d_fns]] == D @@ -647,15 +643,15 @@ Alias: `data` [[date_fn]] === `date` -Returns the current time, or a time parsed from a `string`, as milliseconds since epoch. +Returns the current time, or a time parsed from a specified string, as milliseconds since epoch. *Expression syntax* [source,js] ---- date date value=1558735195 -date “2019-05-24T21:59:55+0000” -date “01/31/2019” format=”MM/DD/YYYY” +date "2019-05-24T21:59:55+0000" +date "01/31/2019" format="MM/DD/YYYY" ---- *Code example* @@ -663,11 +659,11 @@ date “01/31/2019” format=”MM/DD/YYYY” ---- date | formatdate "LLL" -| markdown {context} - font={font family="Arial, sans-serif" size=30 align="left" - color="#000000" - weight="normal" - underline=false +| markdown {context} + font={font family="Arial, sans-serif" size=30 align="left" + color="#000000" + weight="normal" + underline=false italic=false} | render ---- @@ -679,17 +675,15 @@ Using `date` without passing any arguments will return the current date and time |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `value` |`string` -|A date string to be parsed into milliseconds since epoch. The date string be either a valid JavaScript Date input or a string to parse -using the `format` argument. Must be an ISO8601 string or you must provide the format. +|An optional date string that is parsed into milliseconds since epoch. The date string can be either a valid JavaScript `Date` input or a string to parse using the `format` argument. Must be an ISO8601 string, or you must provide the format. |`format` |`string` -|The MomentJS format for parsing the optional date -`string`. See the https://momentjs.com/docs/#/displaying/[MomentJS documentation]. +|The MomentJS format used to parse the specified date string. For more information, see https://momentjs.com/docs/#/displaying/. |=== *Returns:* `number` @@ -701,35 +695,17 @@ using the `format` argument. Must be an ISO8601 string or you must provide the f A mock data set that includes project CI times with usernames, countries, and run phases. -*Expression syntax* -[source,js] ----- -demodata -demodata “ci” -demodata type=”shirts” ----- - -*Code example* -[source,text] ----- -filters -| demodata -| table -| render ----- -`demodata` is a mock data set that you can use to start playing around in Canvas. - *Accepts:* `filter` [cols="3*^<"] |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `type` |`string` -|The name of the demo data set to use. +|The name of the demo data set to use. Default: `"ci"` |=== @@ -741,24 +717,7 @@ Default: `"ci"` [[do_fn]] === `do` -Executes multiple sub-expressions, then returns the original _context_. Use for running functions that produce an action or side effect without changing the original _context_. - -*Expression syntax* -[source,js] ----- -do fn={something cool} ----- - -*Code example* -[source,text] ----- -filters -| demodata -| do fn={something cool} -| table -| render ----- -`do` should be used to invoke a function that produces as a side effect without changing the `context`. +Executes multiple sub-expressions, then returns the original _context_. Use for running functions that produce an action or a side effect without changing the original _context_. *Accepts:* `any` @@ -768,12 +727,12 @@ filters |_Unnamed_ † -Aliases: `expression`, `exp`, `fn`, `function` +Aliases: `exp`, `expression`, `fn`, `function` |`any` -|The sub-expressions to execute. The return values of these sub-expressions are not available in the root pipeline as this function simply returns the _context_. +|The sub-expressions to execute. The return values of these sub-expressions are not available in the root pipeline as this function simply returns the original _context_. |=== -*Returns:* Original _context_ +*Returns:* Depends on your input and arguments [float] @@ -793,7 +752,7 @@ dropdownControl valueColumn=agent filterColumn=agent.keyword filterGroup=group1 [source,text] ---- demodata -| dropdownControl valueColumn=project filterColumn=project +| dropdownControl valueColumn=project filterColumn=project | render ---- This creates a dropdown filter element. It requires a data source and uses the unique values from the given `valueColumn` (i.e. `project`) and applies the filter to the `project` column. Note: `filterColumn` should point to a keyword type field for Elasticsearch data sources. @@ -808,18 +767,17 @@ This creates a dropdown filter element. It requires a data source and uses the u |`string` |The column or field that you want to filter. -|`valueColumn` *** -|`string` -|The column or field to extract the unique values for the drop-down control. - |`filterGroup` |`string` |The group name for the filter. + +|`valueColumn` *** +|`string` +|The column or field from which to extract the unique values for the dropdown control. |=== *Returns:* `render` - [float] [[e_fns]] == E @@ -836,7 +794,7 @@ Returns whether the _context_ is equal to the argument. eq true eq null eq 10 -eq “foo” +eq "foo" ---- *Code example* @@ -844,18 +802,18 @@ eq “foo” ---- filters | demodata -| mapColumn project - fn=${getCell project | - switch +| mapColumn project + fn={getCell project | + switch {case if={eq kibana} then=kibana} {case if={eq elasticsearch} then=elasticsearch} default="other" } | pointseries size="size(cost)" color="project" -| pie +| pie | render ---- -This changes all values in the project column that don’t equal `“kibana”` or `“elasticsearch”` to `“other”`. +This changes all values in the project column that don’t equal `"kibana"` or `"elasticsearch"` to `"other"`. *Accepts:* `boolean`, `number`, `string`, `null` @@ -863,7 +821,7 @@ This changes all values in the project column that don’t equal `“kibana”` |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`boolean`, `number`, `string`, `null` @@ -877,29 +835,7 @@ Alias: `value` [[escount_fn]] === `escount` -Queries {es} for the number of hits matching the specified query. - -*Expression syntax* -[source,js] ----- -escount index=”logstash-*” -escount "currency:\"EUR\"" index=”kibana_sample_data_ecommerce” -escount query="response:404" index=”kibana_sample_data_logs” ----- - -*Code example* -[source,text] ----- -filters -| escount "Cancelled:true" index="kibana_sample_data_flights" -| math "value" -| progress shape="semicircle" - label={formatnumber 0,0} - font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center} - max={filters | escount index="kibana_sample_data_flights"} -| render ----- -The first `escount` expression retrieves the number of flights that were cancelled. The second `escount` expression retrieves the total number of flights. +Query Elasticsearch for the number of hits matching the specified query. *Accepts:* `filter` @@ -907,9 +843,9 @@ The first `escount` expression retrieves the number of flights that were cancell |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ -Alias: `q`, `query` +Aliases: `q`, `query` |`string` |A Lucene query string. @@ -929,36 +865,7 @@ Default: `_all` [[esdocs_fn]] === `esdocs` -Queries {es} for raw documents. Specify the fields you want to retrieve, -especially if you are asking for a lot of rows. - -*Expression syntax* -[source,js] ----- -esdocs index=”logstash-*” -esdocs "currency:\"EUR\"" index=”kibana_sample_data_ecommerce” -esdocs query="response:404" index=”kibana_sample_data_logs” -esdocs index=”kibana_sample_data_flights” count=100 -esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc" ----- - -*Code example* -[source,text] ----- -filters -| esdocs index="kibana_sample_data_ecommerce" - fields="customer_gender, taxful_total_price, order_date" - sort="order_date, asc" - count=10000 -| mapColumn "order_date" - fn={getCell "order_date" | date {context} | rounddate "YYYY-MM-DD"} -| alterColumn "order_date" type="date" -| pointseries x="order_date" y="sum(taxful_total_price)" color="customer_gender" -| plot defaultStyle={seriesStyle lines=3} - palette={palette "#7ECAE3" "#003A4D" gradient=true} -| render ----- -This retrieves the latest 10000 documents data from the `kibana_sample_data_ecommerce` index sorted by `order_date` in ascending order and only requests the `customer_gender`, `taxful_total_price`, and `order_date` fields. +Query Elasticsearch for raw documents. Specify the fields you want to retrieve, especially if you are asking for a lot of rows. *Accepts:* `filter` @@ -966,7 +873,7 @@ This retrieves the latest 10000 documents data from the `kibana_sample_data_ecom |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Aliases: `q`, `query` |`string` @@ -986,9 +893,13 @@ Default: `1000` |`index` |`string` -|An index or index pattern. For example, `"logstash-*"`. +|An index or index pattern. For example, `"logstash-*"`. -Default: `"_all"` +Default: `_all` + +|`metaFields` +|`string` +|Comma separated list of meta fields. For example, `"_index,_type"`. |`sort` |`string` @@ -1002,22 +913,7 @@ Default: `"_all"` [[essql_fn]] === `essql` -Queries {es} using {es} SQL. - -*Expression syntax* -[source,js] ----- -essql query=”SELECT * FROM \”logstash*\”” -essql “SELECT * FROM \”apm*\”” count=10000 ----- - -*Code example* -[source,text] ----- -filters -| essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM \"kibana_sample_data_flights\"" ----- -This retrieves the `Carrier`, `FlightDelayMin`, and `AvgTicketPrice` fields from the “kibana_sample_data_flights” index. +Queries Elasticsearch using Elasticsearch SQL. *Accepts:* `filter` @@ -1025,19 +921,21 @@ This retrieves the `Carrier`, `FlightDelayMin`, and `AvgTicketPrice` fields from |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ -Alias: `q`, `query` +Aliases: `q`, `query` |`string` -|An {es} SQL query. +|An Elasticsearch SQL query. |`count` |`number` -|The number of documents to retrieve. Smaller numbers perform better. +|The number of documents to retrieve. For better performance, use a smaller data set. Default: `1000` |`timezone` + +Alias: `tz` |`string` |The timezone to use for date operations. Valid ISO8601 formats and UTC offsets both work. @@ -1056,9 +954,9 @@ Creates a filter that matches a given column to an exact value. *Expression syntax* [source,js] ---- -exactly “state” value=”running” -exactly “age” value=50 filterGroup=”group2” -exactly column=“project” value=”beats” +exactly "state" value="running" +exactly "age" value=50 filterGroup="group2" +exactly column="project" value="beats" ---- *Code example* @@ -1071,7 +969,7 @@ filters | plot defaultStyle={seriesStyle bars=1} | render ---- -The `exactly` filter here is added to existing filters retrieved by the `filters` function and further filters down the data to only have `”elasticsearch”` data. The `exactly` filter only applies to this one specific element and will not affect other elements in the workpad. +The `exactly` filter here is added to existing filters retrieved by the `filters` function and further filters down the data to only have `"elasticsearch"` data. The `exactly` filter only applies to this one specific element and will not affect other elements in the workpad. *Accepts:* `filter` @@ -1081,25 +979,23 @@ The `exactly` filter here is added to existing filters retrieved by the `filters |`column` *** -Aliases: `field`, `c` +Aliases: `c`, `field` |`string` |The column or field that you want to filter. +|`filterGroup` +|`string` +|The group name for the filter. + |`value` *** Aliases: `v`, `val` |`string` -|The value to match exactly, including white space and -capitalization. - -|`filterGroup` -|`string` -|The group name for the filter. +|The value to match exactly, including white space and capitalization. |=== *Returns:* `filter` - [float] [[f_fns]] == F @@ -1113,8 +1009,8 @@ Filters rows in a `datatable` based on the return value of a sub-expression. *Expression syntax* [source,js] ---- -filterrows {getCell “project” | eq “kibana”} -filterrows fn={getCell “age” | gt 50} +filterrows {getCell "project" | eq "kibana"} +filterrows fn={getCell "age" | gt 50} ---- *Code example* @@ -1123,11 +1019,11 @@ filterrows fn={getCell “age” | gt 50} filters | demodata | filterrows {getCell "country" | any {eq "IN"} {eq "US"} {eq "CN"}} -| mapColumn "@timestamp" +| mapColumn "@timestamp" fn={getCell "@timestamp" | rounddate "YYYY-MM"} | alterColumn "@timestamp" type="date" | pointseries x="@timestamp" y="mean(cost)" color="country" -| plot defaultStyle={seriesStyle points="2" lines="1"} +| plot defaultStyle={seriesStyle points="2" lines="1"} palette={palette "#01A4A4" "#CC6666" "#D0D102" "#616161" "#00A1CB" "#32742C" "#F18D05" "#113F8C" "#61AE24" "#D70060" gradient=false} | render ---- @@ -1139,13 +1035,11 @@ This uses `filterrows` to only keep data from India (`IN`), the United States (` |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** -Aliases: `fn`, `exp`, `expression` +Aliases: `exp`, `expression`, `fn`, `function` |`boolean` -|An expression to pass into each row in the `datatable.` -The expression should return a `boolean`. A `true` value preserves the row, -and a `false` value removes it. +|An expression to pass into each row in the `datatable`. The expression should return a `boolean`. A `true` value preserves the row, and a `false` value removes it. |=== *Returns:* `datatable` @@ -1161,8 +1055,8 @@ Aggregates element filters from the workpad for use elsewhere, usually a data so [source,js] ---- filters -filters group=”timefilter1” -filters group=”timefilter2” group=”dropdownfilter1” ungrouped=true +filters group="timefilter1" +filters group="timefilter2" group="dropdownfilter1" ungrouped=true ---- *Code example* @@ -1171,19 +1065,19 @@ filters group=”timefilter2” group=”dropdownfilter1” ungrouped=true filters group=group2 ungrouped=true | demodata | pointseries x="project" y="size(cost)" color="project" -| plot defaultStyle={seriesStyle bars=0.75} legend=false +| plot defaultStyle={seriesStyle bars=0.75} legend=false font={ - font size=14 - family="'Open Sans', Helvetica, Arial, sans-serif" - align="left" - color="#FFFFFF" - weight="lighter" - underline=true + font size=14 + family="'Open Sans', Helvetica, Arial, sans-serif" + align="left" + color="#FFFFFF" + weight="lighter" + underline=true italic=true } | render ---- -`filters` sets the existing filters as context and accepts `group` parameter to create filter groups. +`filters` sets the existing filters as context and accepts a `group` parameter to opt into specific filter groups. Setting `ungrouped` to `true` opts out of using global filters. *Accepts:* `null` @@ -1191,7 +1085,7 @@ filters group=group2 ungrouped=true |=== |Argument |Type |Description -|_Unnamed_ † +|_Unnamed_ † Alias: `group` |`string` @@ -1199,7 +1093,7 @@ Alias: `group` |`ungrouped` -Alias: `nogroup`, `nogroups` +Aliases: `nogroup`, `nogroups` |`boolean` |Exclude filters that belong to a filter group? @@ -1234,14 +1128,14 @@ font lHeight=32 filters | demodata | pointseries x="project" y="size(cost)" color="project" -| plot defaultStyle={seriesStyle bars=0.75} legend=false +| plot defaultStyle={seriesStyle bars=0.75} legend=false font={ - font size=14 - family="'Open Sans', Helvetica, Arial, sans-serif" - align="left" - color="#FFFFFF" - weight="lighter" - underline=true + font size=14 + family="'Open Sans', Helvetica, Arial, sans-serif" + align="left" + color="#FFFFFF" + weight="lighter" + underline=true italic=true } | render @@ -1255,7 +1149,7 @@ filters |`align` |`string` -|The horizontal alignment of text. +|The horizontal text alignment. Default: `left` @@ -1265,40 +1159,41 @@ Default: `left` |`family` |`string` -|An acceptable CSS web font string. +|An acceptable CSS web font string Default: `"'Open Sans', Helvetica, Arial, sans-serif"` |`italic` |`boolean` -|Italicize the text? +|Italicize the text? Default: `false` -|`lHeight` +|`lHeight` Alias: `lineHeight` |`number`, `null` -|The line height in pixels. +|The line height in pixels + +Default: `null` |`size` |`number` -|The font size in pixels. +|The font size in pixels Default: `14` |`underline` |`boolean` -|Underline the text? +|Underline the text? Default: `false` |`weight` |`string` -|The font weight. For example, `"normal"`, `"bold"`, `"bolder"`, `"lighter"`, `"100"`, `"200"`, `"300"`, `"400"`, `"500"`, `"600"`, `"700"`, `"800"`, or `"900"`. - -Default: `"normal"` +|The font weight. For example, `"normal"`, `"bold"`, `"bolder"`, `"lighter"`, `"100"`, `"200"`, `"300"`, `"400"`, `"500"`, `"600"`, `"700"`, `"800"`, or `"900"`. +Default: `normal` |=== *Returns:* `style` @@ -1313,8 +1208,8 @@ Formats an ISO8601 date string or a date in milliseconds since epoch using Momen *Expression syntax* [source,js] ---- -formatdate format=”YYYY-MM-DD” -formatdate “MM/DD/YYYY” +formatdate format="YYYY-MM-DD" +formatdate "MM/DD/YYYY" ---- *Code example* @@ -1327,7 +1222,7 @@ filters | plot defaultStyle={seriesStyle points=5} | render ---- -This transforms the dates in the `time` field into strings that look like `“Jan ‘19”`, `“Feb ‘19”`, etc. using a MomentJS format. +This transforms the dates in the `time` field into strings that look like `"Jan ‘19"`, `"Feb ‘19"`, etc. using a MomentJS format. *Accepts:* `number`, `string` @@ -1335,7 +1230,7 @@ This transforms the dates in the `time` field into strings that look like `“Ja |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `format` |`string` @@ -1349,13 +1244,13 @@ Alias: `format` [[formatnumber_fn]] === `formatnumber` -Formats a `number` into a formatted `string` using NumeralJS. See http://numeraljs.com/#format. +Formats a number into a formatted number string using NumeralJS. For more information, see http://numeraljs.com/#format. *Expression syntax* [source,js] ---- -formatnumber format=”$0,0.00” -formatnumber “0.0a” +formatnumber format="$0,0.00" +formatnumber "0.0a" ---- *Code example* @@ -1364,8 +1259,8 @@ formatnumber “0.0a” filters | demodata | math "mean(percent_uptime)" -| progress shape="gauge" - label={formatnumber "0%"} +| progress shape="gauge" + label={formatnumber "0%"} font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align="center"} | render ---- @@ -1377,7 +1272,7 @@ The `formatnumber` subexpression receives the same `context` as the `progress` f |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `format` |`string` @@ -1386,7 +1281,6 @@ Alias: `format` *Returns:* `string` - [float] [[g_fns]] == G @@ -1403,13 +1297,13 @@ Fetches a single cell from a `datatable`. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ -Aliases: `column`, `c` +Aliases: `c`, `column` |`string` |The name of the column to fetch the value from. If not provided, the value is retrieved from the first column. -|`row` +|`row` Alias: `r` |`number` @@ -1418,7 +1312,7 @@ Alias: `r` Default: `0` |=== -*Returns:* Depends on the data in the cell +*Returns:* Depends on your input and arguments [float] @@ -1433,7 +1327,7 @@ Returns whether the _context_ is greater than the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`number`, `string` @@ -1455,7 +1349,7 @@ Returns whether the _context_ is greater or equal to the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`number`, `string` @@ -1464,7 +1358,6 @@ Alias: `value` *Returns:* `boolean` - [float] [[h_fns]] == H @@ -1481,7 +1374,7 @@ Retrieves the first N rows from the `datatable`. See also <>. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `count` |`number` @@ -1492,7 +1385,6 @@ Default: `1` *Returns:* `datatable` - [float] [[i_fns]] == I @@ -1509,22 +1401,22 @@ Performs conditional logic. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ *** Alias: `condition` |`boolean` |A `true` or `false` indicating whether a condition is met, usually returned by a sub-expression. When unspecified, the original _context_ is returned. -|`then` -|`any` -|The return value when the condition is `true`. When unspecified and the condition is met, the original _context_ is returned. - |`else` |`any` |The return value when the condition is `false`. When unspecified and the condition is not met, the original _context_ is returned. + +|`then` +|`any` +|The return value when the condition is `true`. When unspecified and the condition is met, the original _context_ is returned. |=== -*Returns:* Depends on your _context_ and arguments +*Returns:* Depends on your input and arguments [float] @@ -1539,37 +1431,31 @@ Displays an image. Provide an image asset as a `base64` data URL, or pass in a s |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Aliases: `dataurl`, `url` |`string`, `null` |The HTTP(S) URL or `base64` data URL of an image. +Example value for the _Unnamed_ argument, formatted as a `base64` data URL: +[source, url] +------------ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+ +------------ + |`mode` |`string` -|`"contain"` shows the entire image, scaled to fit. -`"cover"` fills the container with the image, cropping from the sides or bottom as needed. -`"stretch"` resizes the height and width of the image to 100% of the container. - +|`"contain"` shows the entire image, scaled to fit. `"cover"` fills the container with the image, cropping from the sides or bottom as needed. `"stretch"` resizes the height and width of the image to 100% of the container. Default: `"contain"` |=== -Example value for the `dataurl` argument, formatted as a `base64` data URL: -[source, url] -------------- -data:image/svg+xml;`base64`,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+ -------------- - *Returns:* `image` - [float] [[j_fns]] == J -[float] - [float] [[joinRows_fn]] === `joinRows` @@ -1582,47 +1468,44 @@ Concatenates values from rows in a `datatable` into a single string. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `column` |`string` -|The column or field from which to extract the values. +|The column or field from which to extract the values. -|distinct +|`distinct` |`boolean` -|Extract only unique values? +|Extract only unique values? Default: `true` -|quote +|`quote` |`string` -|The quote character to wrap around each extracted value. +|The quote character to wrap around each extracted value. Default: `"'"` -|separator +|`separator` -Aliases: `sep`, `delimiter` +Aliases: `delimiter`, `sep` |`string` |The delimiter to insert between each extracted value. -Default: `", "` +Default: `","` |=== *Returns:* `string` - [float] [[l_fns]] == L -[float] [float] [[location_fn]] === `location` -Find your current location using the Geolocation API of the browser. Performance can vary, but is fairly accurate. -See: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation. +Find your current location using the Geolocation API of the browser. Performance can vary, but is fairly accurate. See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation. Don’t use <> if you plan to generate PDFs as this function requires user input. *Accepts:* `null` @@ -1641,7 +1524,7 @@ Returns whether the _context_ is less than the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`number`, `string` @@ -1663,7 +1546,7 @@ Returns whether the _context_ is less than or equal to the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`number`, `string` @@ -1672,7 +1555,6 @@ Alias: `value` *Returns:* `boolean` - [float] [[m_fns]] == M @@ -1681,7 +1563,7 @@ Alias: `value` [[mapColumn_fn]] === `mapColumn` -Adds a column calculated as the result of other columns. Changes are made only when you provide arguments. See also <> and <>. +Adds a column calculated as the result of other columns. Changes are made only when you provide arguments.See also <> and <>. *Accepts:* `datatable` @@ -1689,15 +1571,15 @@ Adds a column calculated as the result of other columns. Changes are made only w |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** -Alias: `column` +Aliases: `column`, `name` |`string` |The name of the resulting column. |`expression` *** -Alias: `exp`, `fn`, `function` +Aliases: `exp`, `fn`, `function` |`boolean`, `number`, `string`, `null` |A Canvas expression that is passed to each row as a single row `datatable`. |=== @@ -1719,17 +1601,17 @@ Adds an element that renders Markdown text. TIP: Use the <> functio |_Unnamed_ † -Alias: `expression`, `content` +Aliases: `content`, `expression` |`string` -|A string of text that contains Markdown. To concatenate, pass the <> function multiple times. +|A string of text that contains Markdown. To concatenate, pass the `string` function multiple times. Default: `""` |`font` |`style` -|The CSS font properties for the content. For example, `font-family` or `font-weight`. +|The CSS font properties for the content. For example, "font-family" or "font-weight". -Default: `{font}` +Default: `${font}` |=== *Returns:* `render` @@ -1739,7 +1621,7 @@ Default: `{font}` [[math_fn]] === `math` -Interprets a `TinyMath` math expression using a `number` or `datatable` as _context_. The `datatable` columns are available by their column name. If the _context_ is a `number`, it is available as `value`. +Interprets a `TinyMath` math expression using a `number` or `datatable` as _context_. The `datatable` columns are available by their column name. If the _context_ is a number it is available as `value`. *Accepts:* `number`, `datatable` @@ -1747,11 +1629,11 @@ Interprets a `TinyMath` math expression using a `number` or `datatable` as _cont |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ Alias: `expression` |`string` -|An evaluated TinyMath expression. See <>. +|An evaluated `TinyMath` expression. See https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html. |=== *Returns:* `number` @@ -1771,7 +1653,7 @@ Displays a number over a label. |_Unnamed_ -Aliases: `label`, `text`, `description` +Aliases: `description`, `label`, `text` |`string` |The text describing the metric. @@ -1781,13 +1663,13 @@ Default: `""` |`style` |The CSS font properties for the label. For example, `font-family` or `font-weight`. -Default: `{font size=14 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}`. +Default: `${font size=14 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}` |`metricFont` |`style` |The CSS font properties for the metric. For example, `font-family` or `font-weight`. -Default: `{font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center lHeight=48}`. +Default: `${font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center lHeight=48}` |`metricFormat` @@ -1798,7 +1680,6 @@ Alias: `format` *Returns:* `render` - [float] [[n_fns]] == N @@ -1815,7 +1696,7 @@ Returns whether the _context_ is not equal to the argument. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** Alias: `value` |`boolean`, `number`, `string`, `null` @@ -1824,7 +1705,6 @@ Alias: `value` *Returns:* `boolean` - [float] [[p_fns]] == P @@ -1841,7 +1721,7 @@ Creates a color palette. |=== |Argument |Type |Description -|_Unnamed_ *** † +|_Unnamed_ † Alias: `color` |`string` @@ -1849,7 +1729,7 @@ Alias: `color` |`gradient` |`boolean` -|Make a gradient where supported? +|Make a gradient palette where supported? Default: `false` @@ -1879,17 +1759,17 @@ Configures a pie chart element. |`style` |The CSS font properties for the labels. For example, `font-family` or `font-weight`. -Default: `{font}` +Default: `${font}` |`hole` |`number` -|Draws a hole in the pie, 0-100, as a percentage of the pie radius. +|Draws a hole in the pie, between `0` and `100`, as a percentage of the pie radius. Default: `0` |`labelRadius` |`number` -|The percentage of the container area to use as a radius for the label circle. +|The percentage of the container area to use as a radius for the label circle. Default: `100` @@ -1901,19 +1781,19 @@ Default: `true` |`legend` |`string`, `boolean` -|The legend position. For example, `"nw"`, `"sw"`, `"ne"`, `"se"`. When `false`, the legend is hidden. +|The legend position. For example, `"nw"`, `"sw"`, `"ne"`, `"se"`, or `false`. When `false`, the legend is hidden. Default: `false` |`palette` |`palette` -|A `palette` object for describing the colors to use in this pie chart +|A `palette` object for describing the colors to use in this pie chart. -Default: `{palette}` +Default: `${palette}` |`radius` |`string`, `number` -|The radius of the pie as a percentage (between 0 and 1) of the available space. To automatically set radius, use `"auto"`. +|The radius of the pie as a percentage, between `0` and `1`, of the available space. To automatically set the radius, use `"auto"`. Default: `"auto"` @@ -1923,7 +1803,7 @@ Default: `"auto"` |`tilt` |`number` -|The percentage of tilt, where 1 is fully vertical, and 0 is completely flat. +|The percentage of tilt where `1` is fully vertical, and `0` is completely flat. Default: `1` |=== @@ -1935,7 +1815,7 @@ Default: `1` [[plot_fn]] === `plot` -Configures a plot element. +Configures a chart element. *Accepts:* `pointseries` @@ -1947,13 +1827,13 @@ Configures a plot element. |`seriesStyle` |The default style to use for every series. -Default: `{seriesStyle points=5}` +Default: `${seriesStyle points=5}` |`font` |`style` |The CSS font properties for the labels. For example, `font-family` or `font-weight`. -Default: `{font}` +Default: `${font}` |`legend` |`string`, `boolean` @@ -1963,9 +1843,9 @@ Default: `"ne"` |`palette` |`palette` -|A `palette` object for describing the colors to use in this chart +|A `palette` object for describing the colors to use in this chart. -Default: `{palette}` +Default: `${palette}` |`seriesStyle` † |`seriesStyle` @@ -1999,15 +1879,15 @@ Subdivides a `datatable` by the unique values of the specified columns, and pass |=== |Argument |Type |Description -|`by` *** † +|`by` † |`string` |The column to subdivide the `datatable`. -|`expression` *** † +|`expression` † -Alias: `fn`, `exp`, `function` +Aliases: `exp`, `fn`, `function` |`datatable` -|An expression to pass into each resulting data table. Expressions must return a `datatable`. Use `as` to turn literals into `datatable`s. Multiple expressions must return the same number of rows. If you need to return a different row count, pipe into another instance of <>. If multiple expressions return `datatable`s with the same column names, the last one wins. +|An expression to pass each resulting `datatable` into. Tips: Expressions must return a `datatable`. Use <> to turn literals into `datatable`s. Multiple expressions must return the same number of rows.If you need to return a different row count, pipe into another instance of <>. If multiple expressions returns the columns with the same name, the last one wins. |=== *Returns:* `datatable` @@ -2017,8 +1897,7 @@ Alias: `fn`, `exp`, `function` [[pointseries_fn]] === `pointseries` -Turns a `datatable` into a point series model. Currently we differentiate measure from dimensions by looking for a <>. If you enter a TinyMath -expression in your argument, Canvas treats that argument as a measure. Otherwise, it is a dimension. Dimensions are combined to create unique keys. Measures are then deduplicated by those keys using the specified TinyMath function. +Turn a `datatable` into a point series model. Currently we differentiate measure from dimensions by looking for a `TinyMath` expression. See https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html. If you enter a `TinyMath` expression in your argument, we treat that argument as a measure, otherwise it is a dimension. Dimensions are combined to create unique keys. Measures are then deduplicated by those keys using the specified `TinyMath` function *Accepts:* `datatable` @@ -2054,7 +1933,7 @@ expression in your argument, Canvas treats that argument as a measure. Otherwise [[progress_fn]] === `progress` -Configures a progress element +Configures a progress element. *Accepts:* `number` @@ -2062,7 +1941,7 @@ Configures a progress element |=== |Argument |Type |Description -| _Unnamed_ +|_Unnamed_ Alias: `shape` |`string` @@ -2086,14 +1965,14 @@ Default: `20` |`style` |The CSS font properties for the label. For example, `font-family` or `font-weight`. -Default: `{font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}` +Default: `${font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center}` |`label` |`boolean`, `string` -|To show or hide the value, use `true` or `false`. Alternatively, provide a string to display as a label. +|To show or hide the label, use `true` or `false`. Alternatively, provide a string to display as a label. Default: `true` - + |`max` |`number` |The maximum value of the progress element. @@ -2115,7 +1994,6 @@ Default: `20` *Returns:* `render` - [float] [[r_fns]] == R @@ -2134,19 +2012,19 @@ Renders the _context_ as a specific element and sets element level options, such |`as` |`string` -|The element type to render. You might want to use a specialized function instead, such as <> or <>. - -|`css` -|`string` -|Any block of custom CSS to be scoped to the element - -Default: `".canvasRenderEl{\n\n}"` +|The element type to render. You probably want a specialized function instead, such as <> or <>. |`containerStyle` |`containerStyle` |The style for the container, including background, border, and opacity. -Default: `{containerStyle}` +Default: `${containerStyle}` + +|`css` +|`string` +|Any block of custom CSS to be scoped to the element. + +Default: `".canvasRenderEl${}"` |=== *Returns:* `render` @@ -2165,20 +2043,26 @@ Configures a repeating image element. |Argument |Type |Description |`emptyImage` -|`string` +|`string`, `null` |Fills the difference between the _context_ and `max` parameter for the element with this image. Provide an image asset as a `base64` data URL, or pass in a sub-expression. Default: `null` |`image` -|`string` +|`string`, `null` |The image to repeat. Provide an image asset as a `base64` data URL, or pass in a sub-expression. +Example value for the `image` argument, formatted as a `base64` data URL: +[source, url] +------------ +data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20viewBox%3D%22-3.948730230331421%20-1.7549896240234375%20245.25946044921875%20241.40370178222656%22%20width%3D%22245.25946044921875%22%20height%3D%22241.40370178222656%22%20style%3D%22enable-background%3Anew%200%200%20686.2%20235.7%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.st0%7Bfill%3A%232D2D2D%3B%7D%0A%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%22matrix%281%2C%200%2C%200%2C%201%2C%200%2C%200%29%22%3E%0A%20%20%20%20%3Cg%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M329.4%2C160.3l4.7-0.5l0.3%2C9.6c-12.4%2C1.7-23%2C2.6-31.8%2C2.6c-11.7%2C0-20-3.4-24.9-10.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-4.9-6.8-7.3-17.4-7.3-31.7c0-28.6%2C11.4-42.9%2C34.1-42.9c11%2C0%2C19.2%2C3.1%2C24.6%2C9.2c5.4%2C6.1%2C8.1%2C15.8%2C8.1%2C28.9l-0.7%2C9.3h-53.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0%2C9%2C1.6%2C15.7%2C4.9%2C20c3.3%2C4.3%2C8.9%2C6.5%2C17%2C6.5C312.8%2C161.2%2C321.1%2C160.9%2C329.4%2C160.3z%20M325%2C124.9c0-10-1.6-17.1-4.8-21.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.2-4.1-8.4-6.2-15.6-6.2c-7.2%2C0-12.7%2C2.2-16.3%2C6.5c-3.6%2C4.3-5.5%2C11.3-5.6%2C20.9H325z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M354.3%2C171.4V64h12.2v107.4H354.3z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M443.5%2C113.5v41.1c0%2C4.1%2C10.1%2C3.9%2C10.1%2C3.9l-0.6%2C10.8c-8.6%2C0-15.7%2C0.7-20-3.4c-9.8%2C4.3-19.5%2C6.1-29.3%2C6.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.5%2C0-13.2-2.1-17.1-6.4c-3.9-4.2-5.9-10.3-5.9-18.3c0-7.9%2C2-13.8%2C6-17.5c4-3.7%2C10.3-6.1%2C18.9-6.9l25.6-2.4v-7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0-5.5-1.2-9.5-3.6-11.9c-2.4-2.4-5.7-3.6-9.8-3.6l-32.1%2C0V87.2h31.3c9.2%2C0%2C15.9%2C2.1%2C20.1%2C6.4C441.4%2C97.8%2C443.5%2C104.5%2C443.5%2C113.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bz%20M393.3%2C146.7c0%2C10%2C4.1%2C15%2C12.4%2C15c7.4%2C0%2C14.7-1.2%2C21.8-3.7l3.7-1.3v-26.9l-24.1%2C2.3c-4.9%2C0.4-8.4%2C1.8-10.6%2C4.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3BC394.4%2C138.7%2C393.3%2C142.2%2C393.3%2C146.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M491.2%2C98.2c-11.8%2C0-17.8%2C4.1-17.8%2C12.4c0%2C3.8%2C1.4%2C6.5%2C4.1%2C8.1c2.7%2C1.6%2C8.9%2C3.2%2C18.6%2C4.9%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc9.7%2C1.7%2C16.5%2C4%2C20.5%2C7.1c4%2C3%2C6%2C8.7%2C6%2C17.1c0%2C8.4-2.7%2C14.5-8.1%2C18.4c-5.4%2C3.9-13.2%2C5.9-23.6%2C5.9c-6.7%2C0-29.2-2.5-29.2-2.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bl0.7-10.6c12.9%2C1.2%2C22.3%2C2.2%2C28.6%2C2.2c6.3%2C0%2C11.1-1%2C14.4-3c3.3-2%2C5-5.4%2C5-10.1c0-4.7-1.4-7.9-4.2-9.6c-2.8-1.7-9-3.3-18.6-4.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-9.6-1.5-16.4-3.7-20.4-6.7c-4-2.9-6-8.4-6-16.3c0-7.9%2C2.8-13.8%2C8.4-17.6c5.6-3.8%2C12.6-5.7%2C20.9-5.7c6.6%2C0%2C29.6%2C1.7%2C29.6%2C1.7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bv10.7C508.1%2C99%2C498.2%2C98.2%2C491.2%2C98.2z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M581.7%2C99.5h-25.9v39c0%2C9.3%2C0.7%2C15.5%2C2%2C18.4c1.4%2C2.9%2C4.6%2C4.4%2C9.7%2C4.4l14.5-1l0.8%2C10.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.3%2C1.2-12.8%2C1.8-16.6%2C1.8c-8.5%2C0-14.3-2.1-17.6-6.2c-3.3-4.1-4.9-12-4.9-23.6V99.5h-11.6V88.9h11.6V63.9h12.1v24.9h25.9V99.5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M598.7%2C78.4V64.3h12.2v14.2H598.7z%20M598.7%2C171.4V88.9h12.2v82.5H598.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M663.8%2C87.2c3.6%2C0%2C9.7%2C0.7%2C18.3%2C2l3.9%2C0.5l-0.5%2C9.9c-8.7-1-15.1-1.5-19.2-1.5c-9.2%2C0-15.5%2C2.2-18.8%2C6.6%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.3%2C4.4-5%2C12.6-5%2C24.5c0%2C11.9%2C1.5%2C20.2%2C4.6%2C24.9c3.1%2C4.7%2C9.5%2C7%2C19.3%2C7l19.2-1.5l0.5%2C10.1c-10.1%2C1.5-17.7%2C2.3-22.7%2C2.3%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-12.7%2C0-21.5-3.3-26.3-9.8c-4.8-6.5-7.3-17.5-7.3-33c0-15.5%2C2.6-26.4%2C7.8-32.6C643%2C90.4%2C651.7%2C87.2%2C663.8%2C87.2z%22%2F%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M236.6%2C123.5c0-19.8-12.3-37.2-30.8-43.9c0.8-4.2%2C1.2-8.4%2C1.2-12.7C207%2C30%2C177%2C0%2C140.2%2C0%26%2310%3B%26%239%3B%26%239%3BC118.6%2C0%2C98.6%2C10.3%2C86%2C27.7c-6.2-4.8-13.8-7.4-21.7-7.4c-19.6%2C0-35.5%2C15.9-35.5%2C35.5c0%2C4.3%2C0.8%2C8.5%2C2.2%2C12.4%26%2310%3B%26%239%3B%26%239%3BC12.6%2C74.8%2C0%2C92.5%2C0%2C112.2c0%2C19.9%2C12.4%2C37.3%2C30.9%2C44c-0.8%2C4.1-1.2%2C8.4-1.2%2C12.7c0%2C36.8%2C29.9%2C66.7%2C66.7%2C66.7%26%2310%3B%26%239%3B%26%239%3Bc21.6%2C0%2C41.6-10.4%2C54.1-27.8c6.2%2C4.9%2C13.8%2C7.6%2C21.7%2C7.6c19.6%2C0%2C35.5-15.9%2C35.5-35.5c0-4.3-0.8-8.5-2.2-12.4%26%2310%3B%26%239%3B%26%239%3BC223.9%2C160.9%2C236.6%2C143.2%2C236.6%2C123.5z%20M91.6%2C34.8c10.9-15.9%2C28.9-25.4%2C48.1-25.4c32.2%2C0%2C58.4%2C26.2%2C58.4%2C58.4%26%2310%3B%26%239%3B%26%239%3Bc0%2C3.9-0.4%2C7.7-1.1%2C11.5l-52.2%2C45.8L93%2C101.5L82.9%2C79.9L91.6%2C34.8z%20M65.4%2C29c6.2%2C0%2C12.1%2C2%2C17%2C5.7l-7.8%2C40.3l-35.5-8.4%26%2310%3B%26%239%3B%26%239%3Bc-1.1-3.1-1.7-6.3-1.7-9.7C37.4%2C41.6%2C49.9%2C29%2C65.4%2C29z%20M9.1%2C112.3c0-16.7%2C11-31.9%2C26.9-37.2L75%2C84.4l9.1%2C19.5l-49.8%2C45%26%2310%3B%26%239%3B%26%239%3BC19.2%2C143.1%2C9.1%2C128.6%2C9.1%2C112.3z%20M145.2%2C200.9c-10.9%2C16.1-29%2C25.6-48.4%2C25.6c-32.3%2C0-58.6-26.3-58.6-58.5c0-4%2C0.4-7.9%2C1.1-11.7%26%2310%3B%26%239%3B%26%239%3Bl50.9-46l52%2C23.7l11.5%2C22L145.2%2C200.9z%20M171.2%2C206.6c-6.1%2C0-12-2-16.9-5.8l7.7-40.2l35.4%2C8.3c1.1%2C3.1%2C1.7%2C6.3%2C1.7%2C9.7%26%2310%3B%26%239%3B%26%239%3BC199.2%2C194.1%2C186.6%2C206.6%2C171.2%2C206.6z%20M200.5%2C160.5l-39-9.1l-10.4-19.8l51-44.7c15.1%2C5.7%2C25.2%2C20.2%2C25.2%2C36.5%26%2310%3B%26%239%3B%26%239%3BC227.4%2C140.1%2C216.4%2C155.3%2C200.5%2C160.5z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E +------------ + |`max` |`number` |The maximum number of times the image can repeat. -Default: `100` +Default: `1000` |`size` |`number` @@ -2187,13 +2071,6 @@ Default: `100` Default: `100` |=== - -Example value for the `image` argument, formatted as a `base64` data URL: -[source, url] ------------- -data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20viewBox%3D%22-3.948730230331421%20-1.7549896240234375%20245.25946044921875%20241.40370178222656%22%20width%3D%22245.25946044921875%22%20height%3D%22241.40370178222656%22%20style%3D%22enable-background%3Anew%200%200%20686.2%20235.7%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.st0%7Bfill%3A%232D2D2D%3B%7D%0A%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%22matrix%281%2C%200%2C%200%2C%201%2C%200%2C%200%29%22%3E%0A%20%20%20%20%3Cg%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M329.4%2C160.3l4.7-0.5l0.3%2C9.6c-12.4%2C1.7-23%2C2.6-31.8%2C2.6c-11.7%2C0-20-3.4-24.9-10.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-4.9-6.8-7.3-17.4-7.3-31.7c0-28.6%2C11.4-42.9%2C34.1-42.9c11%2C0%2C19.2%2C3.1%2C24.6%2C9.2c5.4%2C6.1%2C8.1%2C15.8%2C8.1%2C28.9l-0.7%2C9.3h-53.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0%2C9%2C1.6%2C15.7%2C4.9%2C20c3.3%2C4.3%2C8.9%2C6.5%2C17%2C6.5C312.8%2C161.2%2C321.1%2C160.9%2C329.4%2C160.3z%20M325%2C124.9c0-10-1.6-17.1-4.8-21.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.2-4.1-8.4-6.2-15.6-6.2c-7.2%2C0-12.7%2C2.2-16.3%2C6.5c-3.6%2C4.3-5.5%2C11.3-5.6%2C20.9H325z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M354.3%2C171.4V64h12.2v107.4H354.3z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M443.5%2C113.5v41.1c0%2C4.1%2C10.1%2C3.9%2C10.1%2C3.9l-0.6%2C10.8c-8.6%2C0-15.7%2C0.7-20-3.4c-9.8%2C4.3-19.5%2C6.1-29.3%2C6.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.5%2C0-13.2-2.1-17.1-6.4c-3.9-4.2-5.9-10.3-5.9-18.3c0-7.9%2C2-13.8%2C6-17.5c4-3.7%2C10.3-6.1%2C18.9-6.9l25.6-2.4v-7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0-5.5-1.2-9.5-3.6-11.9c-2.4-2.4-5.7-3.6-9.8-3.6l-32.1%2C0V87.2h31.3c9.2%2C0%2C15.9%2C2.1%2C20.1%2C6.4C441.4%2C97.8%2C443.5%2C104.5%2C443.5%2C113.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bz%20M393.3%2C146.7c0%2C10%2C4.1%2C15%2C12.4%2C15c7.4%2C0%2C14.7-1.2%2C21.8-3.7l3.7-1.3v-26.9l-24.1%2C2.3c-4.9%2C0.4-8.4%2C1.8-10.6%2C4.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3BC394.4%2C138.7%2C393.3%2C142.2%2C393.3%2C146.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M491.2%2C98.2c-11.8%2C0-17.8%2C4.1-17.8%2C12.4c0%2C3.8%2C1.4%2C6.5%2C4.1%2C8.1c2.7%2C1.6%2C8.9%2C3.2%2C18.6%2C4.9%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc9.7%2C1.7%2C16.5%2C4%2C20.5%2C7.1c4%2C3%2C6%2C8.7%2C6%2C17.1c0%2C8.4-2.7%2C14.5-8.1%2C18.4c-5.4%2C3.9-13.2%2C5.9-23.6%2C5.9c-6.7%2C0-29.2-2.5-29.2-2.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bl0.7-10.6c12.9%2C1.2%2C22.3%2C2.2%2C28.6%2C2.2c6.3%2C0%2C11.1-1%2C14.4-3c3.3-2%2C5-5.4%2C5-10.1c0-4.7-1.4-7.9-4.2-9.6c-2.8-1.7-9-3.3-18.6-4.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-9.6-1.5-16.4-3.7-20.4-6.7c-4-2.9-6-8.4-6-16.3c0-7.9%2C2.8-13.8%2C8.4-17.6c5.6-3.8%2C12.6-5.7%2C20.9-5.7c6.6%2C0%2C29.6%2C1.7%2C29.6%2C1.7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bv10.7C508.1%2C99%2C498.2%2C98.2%2C491.2%2C98.2z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M581.7%2C99.5h-25.9v39c0%2C9.3%2C0.7%2C15.5%2C2%2C18.4c1.4%2C2.9%2C4.6%2C4.4%2C9.7%2C4.4l14.5-1l0.8%2C10.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.3%2C1.2-12.8%2C1.8-16.6%2C1.8c-8.5%2C0-14.3-2.1-17.6-6.2c-3.3-4.1-4.9-12-4.9-23.6V99.5h-11.6V88.9h11.6V63.9h12.1v24.9h25.9V99.5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M598.7%2C78.4V64.3h12.2v14.2H598.7z%20M598.7%2C171.4V88.9h12.2v82.5H598.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M663.8%2C87.2c3.6%2C0%2C9.7%2C0.7%2C18.3%2C2l3.9%2C0.5l-0.5%2C9.9c-8.7-1-15.1-1.5-19.2-1.5c-9.2%2C0-15.5%2C2.2-18.8%2C6.6%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.3%2C4.4-5%2C12.6-5%2C24.5c0%2C11.9%2C1.5%2C20.2%2C4.6%2C24.9c3.1%2C4.7%2C9.5%2C7%2C19.3%2C7l19.2-1.5l0.5%2C10.1c-10.1%2C1.5-17.7%2C2.3-22.7%2C2.3%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-12.7%2C0-21.5-3.3-26.3-9.8c-4.8-6.5-7.3-17.5-7.3-33c0-15.5%2C2.6-26.4%2C7.8-32.6C643%2C90.4%2C651.7%2C87.2%2C663.8%2C87.2z%22%2F%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M236.6%2C123.5c0-19.8-12.3-37.2-30.8-43.9c0.8-4.2%2C1.2-8.4%2C1.2-12.7C207%2C30%2C177%2C0%2C140.2%2C0%26%2310%3B%26%239%3B%26%239%3BC118.6%2C0%2C98.6%2C10.3%2C86%2C27.7c-6.2-4.8-13.8-7.4-21.7-7.4c-19.6%2C0-35.5%2C15.9-35.5%2C35.5c0%2C4.3%2C0.8%2C8.5%2C2.2%2C12.4%26%2310%3B%26%239%3B%26%239%3BC12.6%2C74.8%2C0%2C92.5%2C0%2C112.2c0%2C19.9%2C12.4%2C37.3%2C30.9%2C44c-0.8%2C4.1-1.2%2C8.4-1.2%2C12.7c0%2C36.8%2C29.9%2C66.7%2C66.7%2C66.7%26%2310%3B%26%239%3B%26%239%3Bc21.6%2C0%2C41.6-10.4%2C54.1-27.8c6.2%2C4.9%2C13.8%2C7.6%2C21.7%2C7.6c19.6%2C0%2C35.5-15.9%2C35.5-35.5c0-4.3-0.8-8.5-2.2-12.4%26%2310%3B%26%239%3B%26%239%3BC223.9%2C160.9%2C236.6%2C143.2%2C236.6%2C123.5z%20M91.6%2C34.8c10.9-15.9%2C28.9-25.4%2C48.1-25.4c32.2%2C0%2C58.4%2C26.2%2C58.4%2C58.4%26%2310%3B%26%239%3B%26%239%3Bc0%2C3.9-0.4%2C7.7-1.1%2C11.5l-52.2%2C45.8L93%2C101.5L82.9%2C79.9L91.6%2C34.8z%20M65.4%2C29c6.2%2C0%2C12.1%2C2%2C17%2C5.7l-7.8%2C40.3l-35.5-8.4%26%2310%3B%26%239%3B%26%239%3Bc-1.1-3.1-1.7-6.3-1.7-9.7C37.4%2C41.6%2C49.9%2C29%2C65.4%2C29z%20M9.1%2C112.3c0-16.7%2C11-31.9%2C26.9-37.2L75%2C84.4l9.1%2C19.5l-49.8%2C45%26%2310%3B%26%239%3B%26%239%3BC19.2%2C143.1%2C9.1%2C128.6%2C9.1%2C112.3z%20M145.2%2C200.9c-10.9%2C16.1-29%2C25.6-48.4%2C25.6c-32.3%2C0-58.6-26.3-58.6-58.5c0-4%2C0.4-7.9%2C1.1-11.7%26%2310%3B%26%239%3B%26%239%3Bl50.9-46l52%2C23.7l11.5%2C22L145.2%2C200.9z%20M171.2%2C206.6c-6.1%2C0-12-2-16.9-5.8l7.7-40.2l35.4%2C8.3c1.1%2C3.1%2C1.7%2C6.3%2C1.7%2C9.7%26%2310%3B%26%239%3B%26%239%3BC199.2%2C194.1%2C186.6%2C206.6%2C171.2%2C206.6z%20M200.5%2C160.5l-39-9.1l-10.4-19.8l51-44.7c15.1%2C5.7%2C25.2%2C20.2%2C25.2%2C36.5%26%2310%3B%26%239%3B%26%239%3BC227.4%2C140.1%2C216.4%2C155.3%2C200.5%2C160.5z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E ------------- - *Returns:* `render` @@ -2209,27 +2086,23 @@ Uses a regular expression to replace parts of a string. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ -Alias: `pattern`, `regex` +Aliases: `pattern`, `regex` |`string` -|The text or pattern of a JavaScript regular expression. For example, `"[aeiou]"`. -You can use capturing groups here. +|The text or pattern of a JavaScript regular expression. For example, `"[aeiou]"`. You can use capturing groups here. -|`flags` +|`flags` Alias: `modifiers` -|`datatable` -|Specify flags. See the -https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp[RegExp documentation] -for reference +|`string` +|Specify flags. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp. Default: `"g"` |`replacement` |`string` -|The replacement for the matching parts of `string`. Capturing groups can be accessed -by their index. For example, `$1`. +|The replacement for the matching parts of string. Capturing groups can be accessed by their index. For example, `"$1"`. Default: `""` |=== @@ -2249,28 +2122,29 @@ Configures an image reveal element. |=== |Argument |Type |Description +|`emptyImage` +|`string`, `null` +|An optional background image to reveal over. Provide an image asset as a ``base64`` data URL, or pass in a sub-expression. + +Default: `null` + |`image` -|`string` +|`string`, `null` |The image to reveal. Provide an image asset as a `base64` data URL, or pass in a sub-expression. -|`emptyImage` -|`string` -|An optional background image to reveal over. Provide an image asset as a `base64` data URL, or pass in a sub-expression. +Example value for the `image` argument, formatted as a `base64` data URL: +[source, url] +------------ +data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20viewBox%3D%22-3.948730230331421%20-1.7549896240234375%20245.25946044921875%20241.40370178222656%22%20width%3D%22245.25946044921875%22%20height%3D%22241.40370178222656%22%20style%3D%22enable-background%3Anew%200%200%20686.2%20235.7%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.st0%7Bfill%3A%232D2D2D%3B%7D%0A%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%22matrix%281%2C%200%2C%200%2C%201%2C%200%2C%200%29%22%3E%0A%20%20%20%20%3Cg%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M329.4%2C160.3l4.7-0.5l0.3%2C9.6c-12.4%2C1.7-23%2C2.6-31.8%2C2.6c-11.7%2C0-20-3.4-24.9-10.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-4.9-6.8-7.3-17.4-7.3-31.7c0-28.6%2C11.4-42.9%2C34.1-42.9c11%2C0%2C19.2%2C3.1%2C24.6%2C9.2c5.4%2C6.1%2C8.1%2C15.8%2C8.1%2C28.9l-0.7%2C9.3h-53.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0%2C9%2C1.6%2C15.7%2C4.9%2C20c3.3%2C4.3%2C8.9%2C6.5%2C17%2C6.5C312.8%2C161.2%2C321.1%2C160.9%2C329.4%2C160.3z%20M325%2C124.9c0-10-1.6-17.1-4.8-21.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.2-4.1-8.4-6.2-15.6-6.2c-7.2%2C0-12.7%2C2.2-16.3%2C6.5c-3.6%2C4.3-5.5%2C11.3-5.6%2C20.9H325z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M354.3%2C171.4V64h12.2v107.4H354.3z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M443.5%2C113.5v41.1c0%2C4.1%2C10.1%2C3.9%2C10.1%2C3.9l-0.6%2C10.8c-8.6%2C0-15.7%2C0.7-20-3.4c-9.8%2C4.3-19.5%2C6.1-29.3%2C6.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.5%2C0-13.2-2.1-17.1-6.4c-3.9-4.2-5.9-10.3-5.9-18.3c0-7.9%2C2-13.8%2C6-17.5c4-3.7%2C10.3-6.1%2C18.9-6.9l25.6-2.4v-7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0-5.5-1.2-9.5-3.6-11.9c-2.4-2.4-5.7-3.6-9.8-3.6l-32.1%2C0V87.2h31.3c9.2%2C0%2C15.9%2C2.1%2C20.1%2C6.4C441.4%2C97.8%2C443.5%2C104.5%2C443.5%2C113.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bz%20M393.3%2C146.7c0%2C10%2C4.1%2C15%2C12.4%2C15c7.4%2C0%2C14.7-1.2%2C21.8-3.7l3.7-1.3v-26.9l-24.1%2C2.3c-4.9%2C0.4-8.4%2C1.8-10.6%2C4.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3BC394.4%2C138.7%2C393.3%2C142.2%2C393.3%2C146.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M491.2%2C98.2c-11.8%2C0-17.8%2C4.1-17.8%2C12.4c0%2C3.8%2C1.4%2C6.5%2C4.1%2C8.1c2.7%2C1.6%2C8.9%2C3.2%2C18.6%2C4.9%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc9.7%2C1.7%2C16.5%2C4%2C20.5%2C7.1c4%2C3%2C6%2C8.7%2C6%2C17.1c0%2C8.4-2.7%2C14.5-8.1%2C18.4c-5.4%2C3.9-13.2%2C5.9-23.6%2C5.9c-6.7%2C0-29.2-2.5-29.2-2.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bl0.7-10.6c12.9%2C1.2%2C22.3%2C2.2%2C28.6%2C2.2c6.3%2C0%2C11.1-1%2C14.4-3c3.3-2%2C5-5.4%2C5-10.1c0-4.7-1.4-7.9-4.2-9.6c-2.8-1.7-9-3.3-18.6-4.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-9.6-1.5-16.4-3.7-20.4-6.7c-4-2.9-6-8.4-6-16.3c0-7.9%2C2.8-13.8%2C8.4-17.6c5.6-3.8%2C12.6-5.7%2C20.9-5.7c6.6%2C0%2C29.6%2C1.7%2C29.6%2C1.7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bv10.7C508.1%2C99%2C498.2%2C98.2%2C491.2%2C98.2z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M581.7%2C99.5h-25.9v39c0%2C9.3%2C0.7%2C15.5%2C2%2C18.4c1.4%2C2.9%2C4.6%2C4.4%2C9.7%2C4.4l14.5-1l0.8%2C10.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.3%2C1.2-12.8%2C1.8-16.6%2C1.8c-8.5%2C0-14.3-2.1-17.6-6.2c-3.3-4.1-4.9-12-4.9-23.6V99.5h-11.6V88.9h11.6V63.9h12.1v24.9h25.9V99.5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M598.7%2C78.4V64.3h12.2v14.2H598.7z%20M598.7%2C171.4V88.9h12.2v82.5H598.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M663.8%2C87.2c3.6%2C0%2C9.7%2C0.7%2C18.3%2C2l3.9%2C0.5l-0.5%2C9.9c-8.7-1-15.1-1.5-19.2-1.5c-9.2%2C0-15.5%2C2.2-18.8%2C6.6%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.3%2C4.4-5%2C12.6-5%2C24.5c0%2C11.9%2C1.5%2C20.2%2C4.6%2C24.9c3.1%2C4.7%2C9.5%2C7%2C19.3%2C7l19.2-1.5l0.5%2C10.1c-10.1%2C1.5-17.7%2C2.3-22.7%2C2.3%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-12.7%2C0-21.5-3.3-26.3-9.8c-4.8-6.5-7.3-17.5-7.3-33c0-15.5%2C2.6-26.4%2C7.8-32.6C643%2C90.4%2C651.7%2C87.2%2C663.8%2C87.2z%22%2F%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M236.6%2C123.5c0-19.8-12.3-37.2-30.8-43.9c0.8-4.2%2C1.2-8.4%2C1.2-12.7C207%2C30%2C177%2C0%2C140.2%2C0%26%2310%3B%26%239%3B%26%239%3BC118.6%2C0%2C98.6%2C10.3%2C86%2C27.7c-6.2-4.8-13.8-7.4-21.7-7.4c-19.6%2C0-35.5%2C15.9-35.5%2C35.5c0%2C4.3%2C0.8%2C8.5%2C2.2%2C12.4%26%2310%3B%26%239%3B%26%239%3BC12.6%2C74.8%2C0%2C92.5%2C0%2C112.2c0%2C19.9%2C12.4%2C37.3%2C30.9%2C44c-0.8%2C4.1-1.2%2C8.4-1.2%2C12.7c0%2C36.8%2C29.9%2C66.7%2C66.7%2C66.7%26%2310%3B%26%239%3B%26%239%3Bc21.6%2C0%2C41.6-10.4%2C54.1-27.8c6.2%2C4.9%2C13.8%2C7.6%2C21.7%2C7.6c19.6%2C0%2C35.5-15.9%2C35.5-35.5c0-4.3-0.8-8.5-2.2-12.4%26%2310%3B%26%239%3B%26%239%3BC223.9%2C160.9%2C236.6%2C143.2%2C236.6%2C123.5z%20M91.6%2C34.8c10.9-15.9%2C28.9-25.4%2C48.1-25.4c32.2%2C0%2C58.4%2C26.2%2C58.4%2C58.4%26%2310%3B%26%239%3B%26%239%3Bc0%2C3.9-0.4%2C7.7-1.1%2C11.5l-52.2%2C45.8L93%2C101.5L82.9%2C79.9L91.6%2C34.8z%20M65.4%2C29c6.2%2C0%2C12.1%2C2%2C17%2C5.7l-7.8%2C40.3l-35.5-8.4%26%2310%3B%26%239%3B%26%239%3Bc-1.1-3.1-1.7-6.3-1.7-9.7C37.4%2C41.6%2C49.9%2C29%2C65.4%2C29z%20M9.1%2C112.3c0-16.7%2C11-31.9%2C26.9-37.2L75%2C84.4l9.1%2C19.5l-49.8%2C45%26%2310%3B%26%239%3B%26%239%3BC19.2%2C143.1%2C9.1%2C128.6%2C9.1%2C112.3z%20M145.2%2C200.9c-10.9%2C16.1-29%2C25.6-48.4%2C25.6c-32.3%2C0-58.6-26.3-58.6-58.5c0-4%2C0.4-7.9%2C1.1-11.7%26%2310%3B%26%239%3B%26%239%3Bl50.9-46l52%2C23.7l11.5%2C22L145.2%2C200.9z%20M171.2%2C206.6c-6.1%2C0-12-2-16.9-5.8l7.7-40.2l35.4%2C8.3c1.1%2C3.1%2C1.7%2C6.3%2C1.7%2C9.7%26%2310%3B%26%239%3B%26%239%3BC199.2%2C194.1%2C186.6%2C206.6%2C171.2%2C206.6z%20M200.5%2C160.5l-39-9.1l-10.4-19.8l51-44.7c15.1%2C5.7%2C25.2%2C20.2%2C25.2%2C36.5%26%2310%3B%26%239%3B%26%239%3BC227.4%2C140.1%2C216.4%2C155.3%2C200.5%2C160.5z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E +------------ |`origin` |`string` -|The position to start the image fill. For example, `"top"`, `"left"`, `"bottom"`, or `"right"` +|The position to start the image fill. For example, `"top"`, `"bottom"`, `"left"`, or right. Default: `"bottom"` |=== - -Example value for the `image` argument, formatted as a `base64` data URL: -[source, url] ------------- -data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20viewBox%3D%22-3.948730230331421%20-1.7549896240234375%20245.25946044921875%20241.40370178222656%22%20width%3D%22245.25946044921875%22%20height%3D%22241.40370178222656%22%20style%3D%22enable-background%3Anew%200%200%20686.2%20235.7%3B%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.st0%7Bfill%3A%232D2D2D%3B%7D%0A%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%22matrix%281%2C%200%2C%200%2C%201%2C%200%2C%200%29%22%3E%0A%20%20%20%20%3Cg%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M329.4%2C160.3l4.7-0.5l0.3%2C9.6c-12.4%2C1.7-23%2C2.6-31.8%2C2.6c-11.7%2C0-20-3.4-24.9-10.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-4.9-6.8-7.3-17.4-7.3-31.7c0-28.6%2C11.4-42.9%2C34.1-42.9c11%2C0%2C19.2%2C3.1%2C24.6%2C9.2c5.4%2C6.1%2C8.1%2C15.8%2C8.1%2C28.9l-0.7%2C9.3h-53.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0%2C9%2C1.6%2C15.7%2C4.9%2C20c3.3%2C4.3%2C8.9%2C6.5%2C17%2C6.5C312.8%2C161.2%2C321.1%2C160.9%2C329.4%2C160.3z%20M325%2C124.9c0-10-1.6-17.1-4.8-21.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.2-4.1-8.4-6.2-15.6-6.2c-7.2%2C0-12.7%2C2.2-16.3%2C6.5c-3.6%2C4.3-5.5%2C11.3-5.6%2C20.9H325z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M354.3%2C171.4V64h12.2v107.4H354.3z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M443.5%2C113.5v41.1c0%2C4.1%2C10.1%2C3.9%2C10.1%2C3.9l-0.6%2C10.8c-8.6%2C0-15.7%2C0.7-20-3.4c-9.8%2C4.3-19.5%2C6.1-29.3%2C6.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.5%2C0-13.2-2.1-17.1-6.4c-3.9-4.2-5.9-10.3-5.9-18.3c0-7.9%2C2-13.8%2C6-17.5c4-3.7%2C10.3-6.1%2C18.9-6.9l25.6-2.4v-7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc0-5.5-1.2-9.5-3.6-11.9c-2.4-2.4-5.7-3.6-9.8-3.6l-32.1%2C0V87.2h31.3c9.2%2C0%2C15.9%2C2.1%2C20.1%2C6.4C441.4%2C97.8%2C443.5%2C104.5%2C443.5%2C113.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bz%20M393.3%2C146.7c0%2C10%2C4.1%2C15%2C12.4%2C15c7.4%2C0%2C14.7-1.2%2C21.8-3.7l3.7-1.3v-26.9l-24.1%2C2.3c-4.9%2C0.4-8.4%2C1.8-10.6%2C4.2%26%2310%3B%26%239%3B%26%239%3B%26%239%3BC394.4%2C138.7%2C393.3%2C142.2%2C393.3%2C146.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M491.2%2C98.2c-11.8%2C0-17.8%2C4.1-17.8%2C12.4c0%2C3.8%2C1.4%2C6.5%2C4.1%2C8.1c2.7%2C1.6%2C8.9%2C3.2%2C18.6%2C4.9%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc9.7%2C1.7%2C16.5%2C4%2C20.5%2C7.1c4%2C3%2C6%2C8.7%2C6%2C17.1c0%2C8.4-2.7%2C14.5-8.1%2C18.4c-5.4%2C3.9-13.2%2C5.9-23.6%2C5.9c-6.7%2C0-29.2-2.5-29.2-2.5%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bl0.7-10.6c12.9%2C1.2%2C22.3%2C2.2%2C28.6%2C2.2c6.3%2C0%2C11.1-1%2C14.4-3c3.3-2%2C5-5.4%2C5-10.1c0-4.7-1.4-7.9-4.2-9.6c-2.8-1.7-9-3.3-18.6-4.8%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-9.6-1.5-16.4-3.7-20.4-6.7c-4-2.9-6-8.4-6-16.3c0-7.9%2C2.8-13.8%2C8.4-17.6c5.6-3.8%2C12.6-5.7%2C20.9-5.7c6.6%2C0%2C29.6%2C1.7%2C29.6%2C1.7%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bv10.7C508.1%2C99%2C498.2%2C98.2%2C491.2%2C98.2z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M581.7%2C99.5h-25.9v39c0%2C9.3%2C0.7%2C15.5%2C2%2C18.4c1.4%2C2.9%2C4.6%2C4.4%2C9.7%2C4.4l14.5-1l0.8%2C10.1%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-7.3%2C1.2-12.8%2C1.8-16.6%2C1.8c-8.5%2C0-14.3-2.1-17.6-6.2c-3.3-4.1-4.9-12-4.9-23.6V99.5h-11.6V88.9h11.6V63.9h12.1v24.9h25.9V99.5z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M598.7%2C78.4V64.3h12.2v14.2H598.7z%20M598.7%2C171.4V88.9h12.2v82.5H598.7z%22%2F%3E%0A%20%20%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M663.8%2C87.2c3.6%2C0%2C9.7%2C0.7%2C18.3%2C2l3.9%2C0.5l-0.5%2C9.9c-8.7-1-15.1-1.5-19.2-1.5c-9.2%2C0-15.5%2C2.2-18.8%2C6.6%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-3.3%2C4.4-5%2C12.6-5%2C24.5c0%2C11.9%2C1.5%2C20.2%2C4.6%2C24.9c3.1%2C4.7%2C9.5%2C7%2C19.3%2C7l19.2-1.5l0.5%2C10.1c-10.1%2C1.5-17.7%2C2.3-22.7%2C2.3%26%2310%3B%26%239%3B%26%239%3B%26%239%3Bc-12.7%2C0-21.5-3.3-26.3-9.8c-4.8-6.5-7.3-17.5-7.3-33c0-15.5%2C2.6-26.4%2C7.8-32.6C643%2C90.4%2C651.7%2C87.2%2C663.8%2C87.2z%22%2F%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Cpath%20class%3D%22st0%22%20d%3D%22M236.6%2C123.5c0-19.8-12.3-37.2-30.8-43.9c0.8-4.2%2C1.2-8.4%2C1.2-12.7C207%2C30%2C177%2C0%2C140.2%2C0%26%2310%3B%26%239%3B%26%239%3BC118.6%2C0%2C98.6%2C10.3%2C86%2C27.7c-6.2-4.8-13.8-7.4-21.7-7.4c-19.6%2C0-35.5%2C15.9-35.5%2C35.5c0%2C4.3%2C0.8%2C8.5%2C2.2%2C12.4%26%2310%3B%26%239%3B%26%239%3BC12.6%2C74.8%2C0%2C92.5%2C0%2C112.2c0%2C19.9%2C12.4%2C37.3%2C30.9%2C44c-0.8%2C4.1-1.2%2C8.4-1.2%2C12.7c0%2C36.8%2C29.9%2C66.7%2C66.7%2C66.7%26%2310%3B%26%239%3B%26%239%3Bc21.6%2C0%2C41.6-10.4%2C54.1-27.8c6.2%2C4.9%2C13.8%2C7.6%2C21.7%2C7.6c19.6%2C0%2C35.5-15.9%2C35.5-35.5c0-4.3-0.8-8.5-2.2-12.4%26%2310%3B%26%239%3B%26%239%3BC223.9%2C160.9%2C236.6%2C143.2%2C236.6%2C123.5z%20M91.6%2C34.8c10.9-15.9%2C28.9-25.4%2C48.1-25.4c32.2%2C0%2C58.4%2C26.2%2C58.4%2C58.4%26%2310%3B%26%239%3B%26%239%3Bc0%2C3.9-0.4%2C7.7-1.1%2C11.5l-52.2%2C45.8L93%2C101.5L82.9%2C79.9L91.6%2C34.8z%20M65.4%2C29c6.2%2C0%2C12.1%2C2%2C17%2C5.7l-7.8%2C40.3l-35.5-8.4%26%2310%3B%26%239%3B%26%239%3Bc-1.1-3.1-1.7-6.3-1.7-9.7C37.4%2C41.6%2C49.9%2C29%2C65.4%2C29z%20M9.1%2C112.3c0-16.7%2C11-31.9%2C26.9-37.2L75%2C84.4l9.1%2C19.5l-49.8%2C45%26%2310%3B%26%239%3B%26%239%3BC19.2%2C143.1%2C9.1%2C128.6%2C9.1%2C112.3z%20M145.2%2C200.9c-10.9%2C16.1-29%2C25.6-48.4%2C25.6c-32.3%2C0-58.6-26.3-58.6-58.5c0-4%2C0.4-7.9%2C1.1-11.7%26%2310%3B%26%239%3B%26%239%3Bl50.9-46l52%2C23.7l11.5%2C22L145.2%2C200.9z%20M171.2%2C206.6c-6.1%2C0-12-2-16.9-5.8l7.7-40.2l35.4%2C8.3c1.1%2C3.1%2C1.7%2C6.3%2C1.7%2C9.7%26%2310%3B%26%239%3B%26%239%3BC199.2%2C194.1%2C186.6%2C206.6%2C171.2%2C206.6z%20M200.5%2C160.5l-39-9.1l-10.4-19.8l51-44.7c15.1%2C5.7%2C25.2%2C20.2%2C25.2%2C36.5%26%2310%3B%26%239%3B%26%239%3BC227.4%2C140.1%2C216.4%2C155.3%2C200.5%2C160.5z%22%2F%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E ------------- - *Returns:* `render` @@ -2286,11 +2160,11 @@ Uses a MomentJS formatting string to round milliseconds since epoch, and returns |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `format` |`string` -|The MomentJS Format to use for bucketing. For example, `"YYYY-MM"` rounds to months. See https://momentjs.com/docs/#/displaying/. +|The MomentJS format to use for bucketing. For example, `"YYYY-MM"` rounds to months. See https://momentjs.com/docs/#/displaying/. |=== *Returns:* `number` @@ -2322,44 +2196,42 @@ Creates an object used for describing the properties of a series on a chart. Use |=== |Argument |Type |Description -|`label` -|`string` -|The name of the series to style. +|`bars` +|`number` +|The width of bars. |`color` |`string` |The line color. +|`fill` +|`number`, `boolean` +|Should we fill in the points? + +Default: `false` + +|`horizontalBars` +|`boolean` +|Sets the orientation of the bars in the chart to horizontal. + +|`label` +|`string` +|The name of the series to style. + |`lines` |`number` |The width of the line. -|`bars` -|`number` -|The width of bars. - |`points` |`number` |The size of points on line. -|`fill` -|`number`, `boolean` -|Should we fill in the points? - -Default: `false` - |`stack` |`number`, `null` |Specifies if the series should be stacked. The number is the stack ID. Series with the same stack ID are stacked together. - -|`horizontalBars` -|`boolean` -|Sets the orientation of the bars in the chart to horizontal. - -Default: `false` |=== -*Returns:* `seriesStyle` +*Returns:* `seriesStyle` [float] @@ -2374,25 +2246,25 @@ Creates a shape. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Alias: `shape` |`string` -|Pick a shape +|Pick a shape. -Default: `"square"` +Default: `square` -|`border` +|`border` -Alias `stroke` -|`number` +Alias: `stroke` +|`string` |An SVG color for the border outlining the shape. -|`borderWidth` +|`borderWidth` Alias: `strokeWidth` |`number` -|The thickness of the border +|The thickness of the border. Default: `0` @@ -2409,7 +2281,7 @@ Default: `"black"` Default: `false` |=== -*Returns:* shape +*Returns:* `shape` [float] @@ -2426,13 +2298,13 @@ Sorts a `datatable` by the specified column. |_Unnamed_ -Alias: `column` +Aliases: `by`, `column` |`string` |The column to sort by. When unspecified, the `datatable` is sorted by the first column. |`reverse` |`boolean` -|Reverse the sorting order? When unspecified, the `datatable` is sorted in ascending order. +|Reverses the sorting order. When unspecified, the `datatable` is sorted in ascending order. Default: `false` |=== @@ -2452,15 +2324,15 @@ Adds a column with the same static value in every row. See also <>. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ Alias: `count` |`number` -|The number of rows to retrieve from the end of the datatable. +|The number of rows to retrieve from the end of the `datatable`. + +Default: `1` |=== *Returns:* `datatable` @@ -2596,29 +2469,29 @@ Creates a time filter for querying a source. |=== |Argument |Type |Description -|`column` +|`column` -Alias: `field`, `c` +Aliases: `c`, `field` |`string` |The column or field that you want to filter. Default: `"@timestamp"` -|`from` - -Alias: `f`, `start` +|`filterGroup` |`string` -|The beginning of the range, in ISO8601 or {es} `datemath` format +|The group name for the filter -|`to` +|`from` -Alias: `t`, `end` +Aliases: `f`, `start` |`string` -|The end of the range, in ISO8601 or {es} `datemath` format +|The beginning of the range, in ISO8601 or Elasticsearch `datemath` format -|`filterGroup` +|`to` + +Aliases: `end`, `t` |`string` -|The group name for the filter +|The end of the range, in ISO8601 or Elasticsearch `datemath` format |=== *Returns:* `filter` @@ -2636,13 +2509,13 @@ Configures a time filter control element. |=== |Argument |Type |Description -|`column` +|`column` -Alias: `field`, `c` +Aliases: `c`, `field` |`string` |The column or field that you want to filter. -Default: `"@timestamp"` +Default: `@timestamp` |`compact` |`boolean` @@ -2655,7 +2528,6 @@ Default: `true` |The group name for the filter. |=== - *Returns:* `render` @@ -2671,37 +2543,37 @@ Uses Timelion to extract one or more time series from many sources. |=== |Argument |Type |Description -|_Unnamed_ +|_Unnamed_ Aliases: `q`, `query` |`string` -|A Timelion query +|A Timelion query Default: `".es(*)"` -|`interval` -|`string` -|The bucket interval for the time series - -Default: `"auto"` - |`from` |`string` -|The {es} `datemath` string for the beginning of the time range. +|The Elasticsearch `datemath` string for the beginning of the time range. Default: `"now-1y"` -|`to` +|`interval` |`string` -|The {es} `datemath` string for the end of the time range. +|The bucket interval for the time series. -Default: `"now"` +Default: `"auto"` |`timezone` |`string` -|The timezone for the time range. See [Moment Timezone](https://momentjs.com/timezone/). +|The timezone for the time range. See https://momentjs.com/timezone/. Default: `"UTC"` + +|`to` +|`string` +|The Elasticsearch `datemath` string for the end of the time range. + +Default: `"now"` |=== *Returns:* `datatable` @@ -2719,16 +2591,15 @@ Explicitly casts the type of the _context_ from one type to the specified type. |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ † Alias: `type` |`string` -|A known type +|A known data type in the expression language. |=== *Returns:* Depends on your input and arguments - [float] [[u_fns]] == U @@ -2737,7 +2608,7 @@ Alias: `type` [[urlparam_fn]] === `urlparam` -Retrieves a URL parameter to use in an expression. The <> function always returns a `string`. For example, you can retrieve the value `"20"` from the parameter `myVar` from the URL `https://localhost:5601/app/canvas?myVar=20`. +Retrieves a URL parameter to use in an expression. The <> function always returns a `string`. For example, you can retrieve the value `"20"` from the parameter `myVar` from the URL `https://localhost:5601/app/canvas?myVar=20`. *Accepts:* `null` @@ -2745,9 +2616,9 @@ Retrieves a URL parameter to use in an expression. The <> function |=== |Argument |Type |Description -|_Unnamed_ *** +|_Unnamed_ *** -Aliases: `var`, `variable` +Aliases: `param`, `var`, `variable` |`string` |The URL hash parameter to retrieve. @@ -2759,4 +2630,3 @@ Default: `""` |=== *Returns:* `string` - From 762c6760f289b3ba98b7b582f7364d814d4253b8 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Fri, 22 Nov 2019 17:17:48 -0600 Subject: [PATCH 036/128] fix management side nav accessibility (#51507) --- .../management/components/sidebar_nav.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/legacy/ui/public/management/components/sidebar_nav.tsx b/src/legacy/ui/public/management/components/sidebar_nav.tsx index f0ac787e0ef44..cd3d85090dce0 100644 --- a/src/legacy/ui/public/management/components/sidebar_nav.tsx +++ b/src/legacy/ui/public/management/components/sidebar_nav.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { EuiIcon, EuiSideNav, IconType } from '@elastic/eui'; +import { EuiIcon, EuiSideNav, IconType, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -72,17 +72,26 @@ export class SidebarNav extends React.Component + <> + +

    + {i18n.translate('common.ui.management.nav.label', { + defaultMessage: 'Management', + })} +

    +
    + + ); } From a2e442209550d09138ffb5e4c2ef7a69bf857c4d Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 22 Nov 2019 17:30:20 -0700 Subject: [PATCH 037/128] [SIEM][Detection Engine] Adds variable for testing rules with Kibana Spaces (#51509) * Added space env that for spaces testing with rules * updated docs * Update x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md Co-Authored-By: Garrett Spong --- .../siem/server/lib/detection_engine/README.md | 11 +++++++++++ .../detection_engine/scripts/delete_signal_by_id.sh | 2 +- .../scripts/delete_signal_by_rule_id.sh | 2 +- .../lib/detection_engine/scripts/find_saved_object.sh | 2 +- .../detection_engine/scripts/find_signal_by_filter.sh | 2 +- .../lib/detection_engine/scripts/find_signals.sh | 2 +- .../lib/detection_engine/scripts/find_signals_sort.sh | 2 +- .../detection_engine/scripts/get_action_instances.sh | 2 +- .../lib/detection_engine/scripts/get_action_types.sh | 2 +- .../detection_engine/scripts/get_alert_instances.sh | 2 +- .../lib/detection_engine/scripts/get_alert_types.sh | 2 +- .../lib/detection_engine/scripts/get_saved_objects.sh | 2 +- .../lib/detection_engine/scripts/get_signal_by_id.sh | 2 +- .../detection_engine/scripts/get_signal_by_rule_id.sh | 2 +- .../lib/detection_engine/scripts/post_signal.sh | 2 +- .../lib/detection_engine/scripts/post_x_signals.sh | 2 +- .../scripts/signals/root_or_admin_saved_query_1.json | 2 +- .../scripts/signals/root_or_admin_saved_query_2.json | 2 +- .../scripts/signals/root_or_admin_saved_query_3.json | 2 +- .../lib/detection_engine/scripts/update_signal.sh | 2 +- 20 files changed, 30 insertions(+), 19 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index c6fc67fde05ed..0a0439a9ace1b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -177,6 +177,17 @@ Every 5 minutes if you get positive hits you will see messages on info like so: server log [09:54:59.013] [info][plugins][siem] Total signals found from signal rule "id: a556065c-0656-4ba1-ad64-a77ca9d2013b", "ruleId: rule-1": 10000 ``` +Signals are space aware and default to the "default" space for these scripts if you do not export +the variable of SPACE_URL. For example, if you want to post rules to the space `test-space` you would +set your SPACE_URL to be: + +```sh +export SPACE_URL=/s/test-space +``` + +So that the scripts prepend a `/s/test-space` in front of all the APIs to correctly create, modify, delete, and update +them from within that space. + See the scripts folder and the tools for more command line fun. Add the `.siem-signals-${your user id}` to your advanced SIEM settings to see any signals diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh index 73882c78edfb8..25cd4bfd33628 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh @@ -13,4 +13,4 @@ set -e curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}/api/detection_engine/rules?id="$1" | jq . + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh index 2b51146e6e1a0..b74ee260ad8ad 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh @@ -13,4 +13,4 @@ set -e curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}/api/detection_engine/rules?rule_id="$1" | jq . + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh index 2b26c939a924c..fbcd159cd24e8 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_saved_object.sh @@ -18,5 +18,5 @@ TYPE=${1:-alert} # https://www.elastic.co/guide/en/kibana/master/saved-objects-api-find.html#saved-objects-api-find-request curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/saved_objects/_find?type=$TYPE \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/saved_objects/_find?type=$TYPE \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh index 6136f66025f3d..34c3c401b4112 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh @@ -17,4 +17,4 @@ FILTER=${1:-'alert.attributes.enabled:%20true'} # Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/detection_engine/rules/_find?filter=$FILTER | jq . + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?filter=$FILTER | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh index 473c786936190..4542eb7c9a827 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh @@ -12,4 +12,4 @@ set -e # Example: ./find_signals.sh curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/detection_engine/rules/_find | jq . + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh index 3f8bab28544e3..122f18bbb80e5 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh @@ -15,5 +15,5 @@ ORDER=${2:-'asc'} # Example: ./find_signals_sort.sh enabled asc curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET "${KIBANA_URL}/api/detection_engine/rules/_find?sort_field=$SORT&sort_order=$ORDER" \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?sort_field=$SORT&sort_order=$ORDER" \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh index e2177bb750057..7804439ce0734 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md#get-apiaction_find-find-actions curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/action/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_find \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh index 7937f2f99a37f..8d8cbdd70a803 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/action/types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/action/types \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh index 3abc8c9adee62..f42d4a52594a7 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/alerting/README.md#get-apialert_find-find-alerts curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/alert/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alert/_find \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh index 7f7361a6252bc..a7c6fa567ecdd 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh @@ -13,5 +13,5 @@ set -e # https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/alerting/README.md#get-apialerttypes-list-alert-types curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/alert/types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alert/types \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh index 4829beba86743..5b5344bc205ff 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_saved_objects.sh @@ -14,5 +14,5 @@ set -e # https://www.elastic.co/guide/en/kibana/master/saved-objects-api-get.html curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/saved_objects/$1/$2 \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/saved_objects/$1/$2 \ | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh index d10f347ff1f9e..239a04846b11a 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh @@ -12,4 +12,4 @@ set -e # Example: ./get_signal_by_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/detection_engine/rules?id="$1" | jq . + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh index 302936fcb523e..5100caac32491 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh @@ -12,4 +12,4 @@ set -e # Example: ./get_signal_by_rule_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}/api/detection_engine/rules?rule_id="$1" | jq . + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh index ee09757835504..8455e7d27ad47 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh @@ -23,7 +23,7 @@ do { -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST ${KIBANA_URL}/api/detection_engine/rules \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ -d "$POST" \ | jq .; } & diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh index 5a4afe6f1806d..8362c576ff554 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh @@ -20,7 +20,7 @@ do { -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST ${KIBANA_URL}/api/detection_engine/rules \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ --data "{ \"rule_id\": \"${i}\", \"risk_score\": \"50\", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json index 4ecda8056837a..d5559ebe23bdb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json @@ -10,5 +10,5 @@ "type": "saved_query", "from": "now-6m", "to": "now", - "saved_id": "Test Query From SIEM" + "saved_id": "test-saveid" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json index 4bf175c1622ab..e272273d817d2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json @@ -10,7 +10,7 @@ "type": "saved_query", "from": "now-6m", "to": "now", - "saved_id": "Test Query From SIEM Two", + "saved_id": "test-saveid-2", "query": "user.name: root or user.name: admin", "language": "kuery" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json index d41921e367d87..9fc2c32c7daf1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json @@ -10,5 +10,5 @@ "type": "saved_query", "from": "now-6m", "to": "now", - "saved_id": "Test Query From SIEM Three" + "saved_id": "test-saveid-3" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh index 30ba9a709f449..6984e7b4c810b 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh @@ -23,7 +23,7 @@ do { -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X PUT ${KIBANA_URL}/api/detection_engine/rules \ + -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ -d "$POST" \ | jq .; } & From bce53ab2b371c583e33ba72dc8ac1f961a917ca7 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 22 Nov 2019 14:52:06 +0000 Subject: [PATCH 038/128] [ML] Restructuring ml plugin in preparation for new platform --- x-pack/legacy/plugins/ml/public/{ => application}/_app.scss | 0 x-pack/legacy/plugins/ml/public/{ => application}/_hacks.scss | 0 x-pack/legacy/plugins/ml/public/{ => application}/_variables.scss | 0 .../plugins/ml/public/{ => application}/access_denied/index.tsx | 0 .../plugins/ml/public/{ => application}/access_denied/page.tsx | 0 x-pack/legacy/plugins/ml/public/{ => application}/app.js | 0 .../annotation_description_list/__snapshots__/index.test.tsx.snap | 0 .../annotations/annotation_description_list/_index.scss | 0 .../annotations/annotation_description_list/index.test.tsx | 0 .../components/annotations/annotation_description_list/index.tsx | 0 .../annotation_flyout/__snapshots__/index.test.tsx.snap | 0 .../components/annotations/annotation_flyout/index.test.tsx | 0 .../components/annotations/annotation_flyout/index.tsx | 0 .../annotations/annotations_table/__mocks__/mock_annotations.json | 0 .../__snapshots__/annotations_table.test.js.snap | 0 .../components/annotations/annotations_table/annotations_table.js | 0 .../annotations/annotations_table/annotations_table.test.js | 0 .../annotations/annotations_table/annotations_table.test.mocks.ts | 0 .../components/annotations/annotations_table/index.js | 0 .../components/annotations/delete_annotation_modal/index.tsx | 0 .../components/anomalies_table/_anomalies_table.scss | 0 .../{ => application}/components/anomalies_table/_index.scss | 0 .../components/anomalies_table/anomalies_table.js | 0 .../components/anomalies_table/anomalies_table.test.js | 0 .../components/anomalies_table/anomalies_table_columns.js | 0 .../components/anomalies_table/anomalies_table_constants.js | 0 .../components/anomalies_table/anomaly_details.js | 0 .../components/anomalies_table/anomaly_details.test.js | 0 .../components/anomalies_table/description_cell.js | 0 .../{ => application}/components/anomalies_table/detector_cell.js | 0 .../public/{ => application}/components/anomalies_table/index.js | 0 .../components/anomalies_table/influencers_cell.js | 0 .../{ => application}/components/anomalies_table/links_menu.js | 0 .../components/anomalies_table/severity_cell/index.ts | 0 .../anomalies_table/severity_cell/severity_cell.test.tsx | 0 .../components/anomalies_table/severity_cell/severity_cell.tsx | 0 .../components/chart_tooltip/_chart_tooltip.scss | 0 .../public/{ => application}/components/chart_tooltip/_index.scss | 0 .../{ => application}/components/chart_tooltip/chart_tooltip.tsx | 0 .../components/chart_tooltip/chart_tooltip_service.d.ts | 0 .../components/chart_tooltip/chart_tooltip_service.js | 0 .../components/chart_tooltip/chart_tooltip_service.test.ts | 0 .../ml/public/{ => application}/components/chart_tooltip/index.ts | 0 .../public/{ => application}/components/controls/_controls.scss | 0 .../ml/public/{ => application}/components/controls/_index.scss | 0 .../controls/checkbox_showcharts/checkbox_showcharts.js | 0 .../components/controls/checkbox_showcharts/index.js | 0 .../ml/public/{ => application}/components/controls/index.js | 0 .../components/controls/select_interval/index.js | 0 .../components/controls/select_interval/select_interval.js | 0 .../components/controls/select_interval/select_interval.test.js | 0 .../components/controls/select_severity/_index.scss | 0 .../components/controls/select_severity/_select_severity.scss | 0 .../components/controls/select_severity/index.js | 0 .../components/controls/select_severity/select_severity.js | 0 .../components/controls/select_severity/select_severity.test.js | 0 .../components/create_job_link_card/create_job_link_card.tsx | 0 .../{ => application}/components/create_job_link_card/index.ts | 0 .../ml/public/{ => application}/components/custom_hooks/index.ts | 0 .../components/custom_hooks/use_partial_state.ts | 0 .../components/data_recognizer/data_recognizer.d.ts | 0 .../components/data_recognizer/data_recognizer.js | 0 .../public/{ => application}/components/data_recognizer/index.ts | 0 .../components/data_recognizer/recognized_result.js | 0 .../{ => application}/components/display_value/display_value.tsx | 0 .../ml/public/{ => application}/components/display_value/index.ts | 0 .../public/{ => application}/components/entity_cell/_index.scss | 0 .../{ => application}/components/entity_cell/entity_cell.js | 0 .../{ => application}/components/entity_cell/entity_cell.scss | 0 .../{ => application}/components/entity_cell/entity_cell.test.js | 0 .../ml/public/{ => application}/components/entity_cell/index.js | 0 .../components/field_title_bar/_field_title_bar.scss | 0 .../{ => application}/components/field_title_bar/_index.scss | 0 .../components/field_title_bar/field_title_bar.js | 0 .../components/field_title_bar/field_title_bar.test.js | 0 .../public/{ => application}/components/field_title_bar/index.js | 0 .../field_type_icon/__snapshots__/field_type_icon.test.js.snap | 0 .../components/field_type_icon/_field_type_icon.scss | 0 .../{ => application}/components/field_type_icon/_index.scss | 0 .../components/field_type_icon/field_type_icon.js | 0 .../components/field_type_icon/field_type_icon.test.js | 0 .../public/{ => application}/components/field_type_icon/index.js | 0 .../__snapshots__/full_time_range_selector.test.tsx.snap | 0 .../full_time_range_selector/full_time_range_selector.test.tsx | 0 .../full_time_range_selector/full_time_range_selector.tsx | 0 .../full_time_range_selector/full_time_range_selector_service.ts | 0 .../components/full_time_range_selector/index.tsx | 0 .../{ => application}/components/influencers_list/_index.scss | 0 .../components/influencers_list/_influencers_list.scss | 0 .../public/{ => application}/components/influencers_list/index.js | 0 .../components/influencers_list/influencers_list.js | 0 .../ml/public/{ => application}/components/items_grid/_index.scss | 0 .../{ => application}/components/items_grid/_items_grid.scss | 0 .../ml/public/{ => application}/components/items_grid/index.js | 0 .../public/{ => application}/components/items_grid/items_grid.js | 0 .../components/items_grid/items_grid_pagination.js | 0 .../public/{ => application}/components/job_message_icon/index.ts | 0 .../components/job_message_icon/job_message_icon.tsx | 0 .../ml/public/{ => application}/components/job_messages/index.ts | 0 .../{ => application}/components/job_messages/job_messages.tsx | 0 .../public/{ => application}/components/job_selector/_index.scss | 0 .../{ => application}/components/job_selector/_job_selector.scss | 0 .../job_selector/custom_selection_table/custom_selection_table.js | 0 .../components/job_selector/custom_selection_table/index.js | 0 .../components/job_selector/id_badges/id_badges.js | 0 .../components/job_selector/id_badges/id_badges.test.js | 0 .../{ => application}/components/job_selector/id_badges/index.js | 0 .../ml/public/{ => application}/components/job_selector/index.js | 0 .../components/job_selector/job_select_service_utils.js | 0 .../{ => application}/components/job_selector/job_selector.js | 0 .../components/job_selector/job_selector_badge/index.js | 0 .../job_selector/job_selector_badge/job_selector_badge.js | 0 .../components/job_selector/job_selector_table/index.js | 0 .../job_selector/job_selector_table/job_selector_table.js | 0 .../job_selector/job_selector_table/job_selector_table.test.js | 0 .../components/job_selector/new_selection_id_badges/index.js | 0 .../new_selection_id_badges/new_selection_id_badges.js | 0 .../new_selection_id_badges/new_selection_id_badges.test.js | 0 .../components/job_selector/timerange_bar/index.js | 0 .../components/job_selector/timerange_bar/timerange_bar.js | 0 .../components/job_selector/timerange_bar/timerange_bar.test.js | 0 .../kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap | 0 .../components/kql_filter_bar/__tests__/utils.js | 0 .../click_outside/__snapshots__/click_outside.test.js.snap | 0 .../components/kql_filter_bar/click_outside/click_outside.js | 0 .../components/kql_filter_bar/click_outside/click_outside.test.js | 0 .../components/kql_filter_bar/click_outside/index.js | 0 .../filter_bar/__snapshots__/filter_bar.test.js.snap | 0 .../components/kql_filter_bar/filter_bar/filter_bar.js | 0 .../components/kql_filter_bar/filter_bar/filter_bar.test.js | 0 .../components/kql_filter_bar/filter_bar/index.js | 0 .../public/{ => application}/components/kql_filter_bar/index.js | 0 .../{ => application}/components/kql_filter_bar/kql_filter_bar.js | 0 .../components/kql_filter_bar/kql_filter_bar.test.js | 0 .../suggestion/__snapshots__/suggestion.test.js.snap | 0 .../components/kql_filter_bar/suggestion/index.js | 0 .../components/kql_filter_bar/suggestion/suggestion.js | 0 .../components/kql_filter_bar/suggestion/suggestion.test.js | 0 .../suggestions/__snapshots__/suggestions.test.js.snap | 0 .../components/kql_filter_bar/suggestions/index.js | 0 .../components/kql_filter_bar/suggestions/suggestions.js | 0 .../components/kql_filter_bar/suggestions/suggestions.test.js | 0 .../public/{ => application}/components/kql_filter_bar/utils.js | 0 .../loading_indicator/__tests__/loading_indicator_directive.js | 0 .../{ => application}/components/loading_indicator/_index.scss | 0 .../components/loading_indicator/_loading_indicator.scss | 0 .../{ => application}/components/loading_indicator/index.js | 0 .../components/loading_indicator/loading_indicator.html | 0 .../components/loading_indicator/loading_indicator.js | 0 .../components/loading_indicator/loading_indicator_directive.js | 0 .../components/loading_indicator/loading_indicator_wrapper.html | 0 .../public/{ => application}/components/message_call_out/index.js | 0 .../components/message_call_out/message_call_out.js | 0 .../ml/public/{ => application}/components/messagebar/index.ts | 0 .../components/messagebar/messagebar_service.d.ts | 0 .../{ => application}/components/messagebar/messagebar_service.js | 0 .../{ => application}/components/ml_in_memory_table/index.ts | 0 .../components/ml_in_memory_table/ml_in_memory_table.tsx | 0 .../{ => application}/components/ml_in_memory_table/types.ts | 0 .../{ => application}/components/navigation_menu/_index.scss | 0 .../components/navigation_menu/_navigation_menu.scss | 0 .../public/{ => application}/components/navigation_menu/index.ts | 0 .../{ => application}/components/navigation_menu/main_tabs.tsx | 0 .../components/navigation_menu/navigation_menu.tsx | 0 .../{ => application}/components/navigation_menu/tabs.test.tsx | 0 .../public/{ => application}/components/navigation_menu/tabs.tsx | 0 .../navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap | 0 .../{ => application}/components/navigation_menu/top_nav/index.ts | 0 .../components/navigation_menu/top_nav/top_nav.test.tsx | 0 .../components/navigation_menu/top_nav/top_nav.tsx | 0 .../{ => application}/components/node_available_warning/index.ts | 0 .../components/node_available_warning/node_available_warning.tsx | 0 .../rule_editor/__snapshots__/actions_section.test.js.snap | 0 .../rule_editor/__snapshots__/condition_expression.test.js.snap | 0 .../rule_editor/__snapshots__/conditions_section.test.js.snap | 0 .../rule_editor/__snapshots__/rule_editor_flyout.test.js.snap | 0 .../rule_editor/__snapshots__/scope_expression.test.js.snap | 0 .../rule_editor/__snapshots__/scope_section.test.js.snap | 0 .../{ => application}/components/rule_editor/__tests__/utils.js | 0 .../public/{ => application}/components/rule_editor/_index.scss | 0 .../{ => application}/components/rule_editor/_rule_editor.scss | 0 .../{ => application}/components/rule_editor/actions_section.js | 0 .../components/rule_editor/actions_section.test.js | 0 .../__snapshots__/detector_description_list.test.js.snap | 0 .../detector_description_list/_detector_description_list.scss | 0 .../rule_editor/components/detector_description_list/_index.scss | 0 .../detector_description_list/detector_description_list.js | 0 .../detector_description_list/detector_description_list.test.js | 0 .../rule_editor/components/detector_description_list/index.js | 0 .../components/rule_editor/condition_expression.js | 0 .../components/rule_editor/condition_expression.test.js | 0 .../components/rule_editor/conditions_section.js | 0 .../components/rule_editor/conditions_section.test.js | 0 .../ml/public/{ => application}/components/rule_editor/index.js | 0 .../components/rule_editor/rule_editor_flyout.js | 0 .../components/rule_editor/rule_editor_flyout.test.js | 0 .../{ => application}/components/rule_editor/scope_expression.js | 0 .../components/rule_editor/scope_expression.test.js | 0 .../{ => application}/components/rule_editor/scope_section.js | 0 .../components/rule_editor/scope_section.test.js | 0 .../__snapshots__/add_to_filter_list_link.test.js.snap | 0 .../__snapshots__/delete_rule_modal.test.js.snap | 0 .../__snapshots__/edit_condition_link.test.js.snap | 0 .../__snapshots__/rule_action_panel.test.js.snap | 0 .../rule_editor/select_rule_action/add_to_filter_list_link.js | 0 .../select_rule_action/add_to_filter_list_link.test.js | 0 .../rule_editor/select_rule_action/delete_rule_modal.js | 0 .../rule_editor/select_rule_action/delete_rule_modal.test.js | 0 .../rule_editor/select_rule_action/edit_condition_link.js | 0 .../rule_editor/select_rule_action/edit_condition_link.test.js | 0 .../components/rule_editor/select_rule_action/index.js | 0 .../rule_editor/select_rule_action/rule_action_panel.js | 0 .../rule_editor/select_rule_action/rule_action_panel.test.js | 0 .../rule_editor/select_rule_action/select_rule_action.js | 0 .../ml/public/{ => application}/components/rule_editor/utils.js | 0 .../ml/public/{ => application}/components/stats_bar/_index.scss | 0 .../ml/public/{ => application}/components/stats_bar/_stat.scss | 0 .../public/{ => application}/components/stats_bar/_stats_bar.scss | 0 .../ml/public/{ => application}/components/stats_bar/index.ts | 0 .../ml/public/{ => application}/components/stats_bar/stat.tsx | 0 .../public/{ => application}/components/stats_bar/stats_bar.tsx | 0 .../ml/public/{ => application}/components/upgrade/index.ts | 0 .../{ => application}/components/upgrade/upgrade_warning.tsx | 0 .../validate_job/__snapshots__/validate_job_view.test.js.snap | 0 .../ml/public/{ => application}/components/validate_job/index.ts | 0 .../components/validate_job/validate_job_view.d.ts | 0 .../components/validate_job/validate_job_view.js | 0 .../components/validate_job/validate_job_view.test.js | 0 .../{ => application}/contexts/kibana/__mocks__/index_pattern.ts | 0 .../{ => application}/contexts/kibana/__mocks__/index_patterns.ts | 0 .../{ => application}/contexts/kibana/__mocks__/kibana_config.ts | 0 .../contexts/kibana/__mocks__/kibana_context_value.ts | 0 .../{ => application}/contexts/kibana/__mocks__/saved_search.ts | 0 .../plugins/ml/public/{ => application}/contexts/kibana/index.ts | 0 .../ml/public/{ => application}/contexts/kibana/kibana_context.ts | 0 .../contexts/kibana/use_current_index_pattern.ts | 0 .../{ => application}/contexts/kibana/use_current_saved_search.ts | 0 .../{ => application}/contexts/kibana/use_kibana_context.ts | 0 .../ml/public/{ => application}/contexts/ui/__mocks__/mocks.ts | 0 .../contexts/ui/__mocks__/use_ui_chrome_context.ts | 0 .../{ => application}/contexts/ui/__mocks__/use_ui_context.ts | 0 .../plugins/ml/public/{ => application}/contexts/ui/index.ts | 0 .../ml/public/{ => application}/contexts/ui/ui_context.tsx | 0 .../public/{ => application}/contexts/ui/use_ui_chrome_context.ts | 0 .../ml/public/{ => application}/contexts/ui/use_ui_context.ts | 0 .../ml/public/{ => application}/data_frame_analytics/_index.scss | 0 .../public/{ => application}/data_frame_analytics/breadcrumbs.ts | 0 .../data_frame_analytics/common/analytics.test.ts | 0 .../{ => application}/data_frame_analytics/common/analytics.ts | 0 .../{ => application}/data_frame_analytics/common/fields.ts | 0 .../public/{ => application}/data_frame_analytics/common/index.ts | 0 .../ml/public/{ => application}/data_frame_analytics/index.ts | 0 .../components/exploration/_exploration.scss | 0 .../analytics_exploration/components/exploration/_index.scss | 0 .../analytics_exploration/components/exploration/common.test.ts | 0 .../pages/analytics_exploration/components/exploration/common.ts | 0 .../components/exploration/exploration.test.tsx | 0 .../analytics_exploration/components/exploration/exploration.tsx | 0 .../pages/analytics_exploration/components/exploration/index.ts | 0 .../components/exploration/use_explore_data.ts | 0 .../components/regression_exploration/_index.scss | 0 .../regression_exploration/_regression_exploration.scss | 0 .../components/regression_exploration/error_callout.tsx | 0 .../components/regression_exploration/evaluate_panel.tsx | 0 .../components/regression_exploration/evaluate_stat.tsx | 0 .../components/regression_exploration/index.ts | 0 .../components/regression_exploration/regression_exploration.tsx | 0 .../components/regression_exploration/results_table.tsx | 0 .../components/regression_exploration/use_explore_data.ts | 0 .../pages/analytics_exploration/directive.tsx | 0 .../data_frame_analytics/pages/analytics_exploration/page.tsx | 0 .../data_frame_analytics/pages/analytics_exploration/route.ts | 0 .../components/analytics_list/__mocks__/analytics_list_item.json | 0 .../components/analytics_list/__mocks__/analytics_stats.json | 0 .../components/analytics_list/_analytics_table.scss | 0 .../analytics_management/components/analytics_list/_index.scss | 0 .../components/analytics_list/action_delete.test.tsx | 0 .../components/analytics_list/action_delete.tsx | 0 .../components/analytics_list/action_start.tsx | 0 .../analytics_management/components/analytics_list/actions.tsx | 0 .../components/analytics_list/analytics_list.tsx | 0 .../analytics_management/components/analytics_list/columns.tsx | 0 .../analytics_management/components/analytics_list/common.test.ts | 0 .../analytics_management/components/analytics_list/common.ts | 0 .../components/analytics_list/expanded_row.tsx | 0 .../components/analytics_list/expanded_row_details_pane.tsx | 0 .../components/analytics_list/expanded_row_json_pane.tsx | 0 .../components/analytics_list/expanded_row_messages_pane.tsx | 0 .../pages/analytics_management/components/analytics_list/index.ts | 0 .../components/analytics_list/progress_bar.tsx | 0 .../components/analytics_list/use_refresh_interval.ts | 0 .../create_analytics_advanced_editor.tsx | 0 .../components/create_analytics_advanced_editor/index.ts | 0 .../create_analytics_button/create_analytics_button.test.tsx | 0 .../create_analytics_button/create_analytics_button.tsx | 0 .../components/create_analytics_button/index.ts | 0 .../create_analytics_flyout/_create_analytics_flyout.scss | 0 .../components/create_analytics_flyout/_index.scss | 0 .../create_analytics_flyout/create_analytics_flyout.test.tsx | 0 .../create_analytics_flyout/create_analytics_flyout.tsx | 0 .../components/create_analytics_flyout/index.ts | 0 .../create_analytics_flyout_wrapper.tsx | 0 .../components/create_analytics_flyout_wrapper/index.ts | 0 .../components/create_analytics_form/_create_analytics_form.scss | 0 .../components/create_analytics_form/_index.scss | 0 .../create_analytics_form/create_analytics_form.test.tsx | 0 .../components/create_analytics_form/create_analytics_form.tsx | 0 .../components/create_analytics_form/index.ts | 0 .../components/create_analytics_form/job_type.tsx | 0 .../components/create_analytics_form/messages.tsx | 0 .../components/refresh_analytics_list_button/index.ts | 0 .../refresh_analytics_list_button.tsx | 0 .../data_frame_analytics/pages/analytics_management/directive.tsx | 0 .../hooks/use_create_analytics_form/actions.ts | 0 .../analytics_management/hooks/use_create_analytics_form/index.ts | 0 .../hooks/use_create_analytics_form/reducer.test.ts | 0 .../hooks/use_create_analytics_form/reducer.ts | 0 .../hooks/use_create_analytics_form/state.test.ts | 0 .../analytics_management/hooks/use_create_analytics_form/state.ts | 0 .../use_create_analytics_form/use_create_analytics_form.test.tsx | 0 .../hooks/use_create_analytics_form/use_create_analytics_form.ts | 0 .../data_frame_analytics/pages/analytics_management/page.tsx | 0 .../data_frame_analytics/pages/analytics_management/route.ts | 0 .../services/analytics_service/delete_analytics.ts | 0 .../services/analytics_service/get_analytics.test.ts | 0 .../services/analytics_service/get_analytics.ts | 0 .../analytics_management/services/analytics_service/index.ts | 0 .../services/analytics_service/start_analytics.ts | 0 .../services/analytics_service/stop_analytics.ts | 0 .../ml/public/{ => application}/datavisualizer/_index.scss | 0 .../ml/public/{ => application}/datavisualizer/breadcrumbs.ts | 0 .../{ => application}/datavisualizer/datavisualizer_selector.tsx | 0 .../ml/public/{ => application}/datavisualizer/directive.tsx | 0 .../datavisualizer/file_based/_file_datavisualizer.scss | 0 .../{ => application}/datavisualizer/file_based/_index.scss | 0 .../{ => application}/datavisualizer/file_based/breadcrumbs.ts | 0 .../datavisualizer/file_based/components/_index.scss | 0 .../file_based/components/about_panel/_about_panel.scss | 0 .../datavisualizer/file_based/components/about_panel/_index.scss | 0 .../file_based/components/about_panel/about_panel.js | 0 .../datavisualizer/file_based/components/about_panel/index.js | 0 .../file_based/components/about_panel/welcome_content.js | 0 .../file_based/components/analysis_summary/_analysis_summary.scss | 0 .../file_based/components/analysis_summary/_index.scss | 0 .../file_based/components/analysis_summary/analysis_summary.js | 0 .../file_based/components/analysis_summary/index.js | 0 .../file_based/components/bottom_bar/bottom_bar.tsx | 0 .../datavisualizer/file_based/components/bottom_bar/index.ts | 0 .../components/edit_flyout/__snapshots__/overrides.test.js.snap | 0 .../file_based/components/edit_flyout/_edit_flyout.scss | 0 .../datavisualizer/file_based/components/edit_flyout/_index.scss | 0 .../file_based/components/edit_flyout/edit_flyout.js | 0 .../datavisualizer/file_based/components/edit_flyout/index.js | 0 .../file_based/components/edit_flyout/options/index.js | 0 .../file_based/components/edit_flyout/options/option_lists.js | 0 .../file_based/components/edit_flyout/options/options.js | 0 .../datavisualizer/file_based/components/edit_flyout/overrides.js | 0 .../file_based/components/edit_flyout/overrides.test.js | 0 .../file_based/components/edit_flyout/overrides_validation.js | 0 .../components/experimental_badge/_experimental_badge.scss | 0 .../file_based/components/experimental_badge/_index.scss | 0 .../components/experimental_badge/experimental_badge.js | 0 .../file_based/components/experimental_badge/index.js | 0 .../file_based/components/fields_stats/_field_stats_card.scss | 0 .../file_based/components/fields_stats/_fields_stats.scss | 0 .../datavisualizer/file_based/components/fields_stats/_index.scss | 0 .../file_based/components/fields_stats/field_stats_card.js | 0 .../file_based/components/fields_stats/fields_stats.js | 0 .../file_based/components/fields_stats/get_field_names.js | 0 .../datavisualizer/file_based/components/fields_stats/index.js | 0 .../file_based/components/file_contents/_file_contents.scss | 0 .../file_based/components/file_contents/_index.scss | 0 .../file_based/components/file_contents/file_contents.js | 0 .../datavisualizer/file_based/components/file_contents/index.js | 0 .../file_datavisualizer_view/_file_datavisualizer_view.scss | 0 .../file_based/components/file_datavisualizer_view/_index.scss | 0 .../file_based/components/file_datavisualizer_view/constants.ts | 0 .../file_datavisualizer_view/file_datavisualizer_view.js | 0 .../components/file_datavisualizer_view/file_error_callouts.js | 0 .../file_based/components/file_datavisualizer_view/index.js | 0 .../datavisualizer/file_based/components/import_errors/errors.js | 0 .../datavisualizer/file_based/components/import_errors/index.js | 0 .../file_based/components/import_progress/import_progress.js | 0 .../datavisualizer/file_based/components/import_progress/index.js | 0 .../file_based/components/import_settings/advanced.js | 0 .../file_based/components/import_settings/import_settings.js | 0 .../datavisualizer/file_based/components/import_settings/index.js | 0 .../file_based/components/import_settings/simple.js | 0 .../file_based/components/import_summary/_import_sumary.scss | 0 .../file_based/components/import_summary/_index.scss | 0 .../file_based/components/import_summary/import_summary.js | 0 .../datavisualizer/file_based/components/import_summary/index.js | 0 .../file_based/components/import_view/import_view.js | 0 .../file_based/components/import_view/importer/csv_importer.js | 0 .../file_based/components/import_view/importer/importer.js | 0 .../components/import_view/importer/importer_factory.js | 0 .../file_based/components/import_view/importer/index.js | 0 .../file_based/components/import_view/importer/ndjson_importer.js | 0 .../file_based/components/import_view/importer/sst_importer.js | 0 .../datavisualizer/file_based/components/import_view/index.js | 0 .../datavisualizer/file_based/components/results_links/index.js | 0 .../file_based/components/results_links/results_links.js | 0 .../datavisualizer/file_based/components/results_view/_index.scss | 0 .../file_based/components/results_view/_results_view.scss | 0 .../datavisualizer/file_based/components/results_view/index.js | 0 .../file_based/components/results_view/results_view.js | 0 .../datavisualizer/file_based/components/utils/index.js | 0 .../datavisualizer/file_based/components/utils/overrides.js | 0 .../datavisualizer/file_based/components/utils/utils.js | 0 .../datavisualizer/file_based/file_datavisualizer.tsx | 0 .../datavisualizer/file_based/file_datavisualizer_directive.tsx | 0 .../public/{ => application}/datavisualizer/file_based/index.ts | 0 .../plugins/ml/public/{ => application}/datavisualizer/index.ts | 0 .../{ => application}/datavisualizer/index_based/_index.scss | 0 .../{ => application}/datavisualizer/index_based/breadcrumbs.ts | 0 .../datavisualizer/index_based/common/field_vis_config.ts | 0 .../{ => application}/datavisualizer/index_based/common/index.ts | 0 .../datavisualizer/index_based/common/request.ts | 0 .../index_based/components/actions_panel/actions_panel.tsx | 0 .../datavisualizer/index_based/components/actions_panel/index.ts | 0 .../index_based/components/field_data_card/_field_data_card.scss | 0 .../index_based/components/field_data_card/_index.scss | 0 .../components/field_data_card/content_types/boolean_content.tsx | 0 .../components/field_data_card/content_types/date_content.tsx | 0 .../field_data_card/content_types/document_count_content.tsx | 0 .../field_data_card/content_types/geo_point_content.tsx | 0 .../index_based/components/field_data_card/content_types/index.ts | 0 .../components/field_data_card/content_types/ip_content.tsx | 0 .../components/field_data_card/content_types/keyword_content.tsx | 0 .../field_data_card/content_types/not_in_docs_content.tsx | 0 .../components/field_data_card/content_types/number_content.tsx | 0 .../components/field_data_card/content_types/other_content.tsx | 0 .../components/field_data_card/content_types/text_content.tsx | 0 .../field_data_card/document_count_chart/document_count_chart.tsx | 0 .../components/field_data_card/document_count_chart/index.ts | 0 .../components/field_data_card/examples_list/example.tsx | 0 .../components/field_data_card/examples_list/examples_list.tsx | 0 .../index_based/components/field_data_card/examples_list/index.ts | 0 .../index_based/components/field_data_card/field_data_card.tsx | 0 .../index_based/components/field_data_card/index.ts | 0 .../components/field_data_card/loading_indicator/index.ts | 0 .../field_data_card/loading_indicator/loading_indicator.tsx | 0 .../components/field_data_card/metric_distribution_chart/index.ts | 0 .../metric_distribution_chart/metric_distribution_chart.tsx | 0 .../metric_distribution_chart_data_builder.tsx | 0 .../metric_distribution_chart_tooltip_header.tsx | 0 .../index_based/components/field_data_card/top_values/index.ts | 0 .../components/field_data_card/top_values/top_values.tsx | 0 .../components/field_types_select/field_types_select.tsx | 0 .../index_based/components/field_types_select/index.ts | 0 .../index_based/components/fields_panel/fields_panel.tsx | 0 .../datavisualizer/index_based/components/fields_panel/index.ts | 0 .../datavisualizer/index_based/components/search_panel/index.ts | 0 .../index_based/components/search_panel/search_panel.tsx | 0 .../datavisualizer/index_based/data_loader/data_loader.ts | 0 .../datavisualizer/index_based/data_loader/index.ts | 0 .../{ => application}/datavisualizer/index_based/directive.tsx | 0 .../public/{ => application}/datavisualizer/index_based/index.ts | 0 .../public/{ => application}/datavisualizer/index_based/page.tsx | 0 .../public/{ => application}/datavisualizer/index_based/route.ts | 0 .../explorer/__mocks__/mock_anomalies_table_data.json | 0 .../explorer/__mocks__/mock_overall_swimlane.json | 0 .../explorer/__snapshots__/explorer_swimlane.test.js.snap | 0 .../{ => application}/explorer/__tests__/explorer_controller.js | 0 .../plugins/ml/public/{ => application}/explorer/_explorer.scss | 0 .../plugins/ml/public/{ => application}/explorer/_index.scss | 0 .../plugins/ml/public/{ => application}/explorer/breadcrumbs.js | 0 .../__snapshots__/explorer_no_influencers_found.test.js.snap | 0 .../explorer_no_influencers_found.js | 0 .../explorer_no_influencers_found.test.js | 0 .../explorer/components/explorer_no_influencers_found/index.js | 0 .../__snapshots__/explorer_no_jobs_found.test.js.snap | 0 .../components/explorer_no_jobs_found/explorer_no_jobs_found.js | 0 .../explorer_no_jobs_found/explorer_no_jobs_found.test.js | 0 .../explorer/components/explorer_no_jobs_found/index.js | 0 .../__snapshots__/explorer_no_results_found.test.js.snap | 0 .../explorer_no_results_found/explorer_no_results_found.js | 0 .../explorer_no_results_found/explorer_no_results_found.test.js | 0 .../explorer/components/explorer_no_results_found/index.js | 0 .../ml/public/{ => application}/explorer/components/index.js | 0 .../plugins/ml/public/{ => application}/explorer/explorer.js | 0 .../explorer_charts/__mocks__/mock_anomaly_chart_records.json | 0 .../explorer/explorer_charts/__mocks__/mock_anomaly_record.json | 0 .../explorer/explorer_charts/__mocks__/mock_chart_data.js | 0 .../explorer/explorer_charts/__mocks__/mock_chart_data_rare.js | 0 .../explorer/explorer_charts/__mocks__/mock_detectors_by_job.json | 0 .../explorer/explorer_charts/__mocks__/mock_job_config.json | 0 .../explorer_charts/__mocks__/mock_series_config_filebeat.json | 0 .../explorer_charts/__mocks__/mock_series_config_rare.json | 0 .../explorer_charts/__mocks__/mock_series_promises_response.json | 0 .../__snapshots__/explorer_chart_config_builder.test.js.snap | 0 .../__snapshots__/explorer_chart_info_tooltip.test.js.snap | 0 .../__snapshots__/explorer_charts_container_service.test.js.snap | 0 .../explorer/explorer_charts/_explorer_chart.scss | 0 .../explorer/explorer_charts/_explorer_chart_tooltip.scss | 0 .../explorer/explorer_charts/_explorer_charts_container.scss | 0 .../public/{ => application}/explorer/explorer_charts/_index.scss | 0 .../__snapshots__/explorer_chart_label.test.js.snap | 0 .../__snapshots__/explorer_chart_label_badge.test.js.snap | 0 .../components/explorer_chart_label/_explorer_chart_label.scss | 0 .../explorer_chart_label/_explorer_chart_label_badge.scss | 0 .../explorer_charts/components/explorer_chart_label/_index.scss | 0 .../components/explorer_chart_label/explorer_chart_label.js | 0 .../components/explorer_chart_label/explorer_chart_label.test.js | 0 .../components/explorer_chart_label/explorer_chart_label_badge.js | 0 .../explorer_chart_label/explorer_chart_label_badge.test.js | 0 .../explorer_charts/components/explorer_chart_label/index.js | 0 .../explorer/explorer_charts/explorer_chart_config_builder.js | 0 .../explorer_charts/explorer_chart_config_builder.test.js | 0 .../explorer/explorer_charts/explorer_chart_distribution.js | 0 .../explorer/explorer_charts/explorer_chart_distribution.test.js | 0 .../explorer_charts/explorer_chart_distribution.test.mocks.ts | 0 .../explorer/explorer_charts/explorer_chart_info_tooltip.js | 0 .../explorer/explorer_charts/explorer_chart_info_tooltip.test.js | 0 .../explorer/explorer_charts/explorer_chart_single_metric.js | 0 .../explorer/explorer_charts/explorer_chart_single_metric.test.js | 0 .../explorer_charts/explorer_chart_single_metric.test.mocks.ts | 0 .../explorer/explorer_charts/explorer_charts_container.js | 0 .../explorer/explorer_charts/explorer_charts_container.test.js | 0 .../explorer_charts/explorer_charts_container.test.mocks.ts | 0 .../explorer/explorer_charts/explorer_charts_container_service.js | 0 .../explorer_charts/explorer_charts_container_service.test.js | 0 .../explorer_charts_container_service.test.mocks.ts | 0 .../ml/public/{ => application}/explorer/explorer_charts/index.js | 0 .../ml/public/{ => application}/explorer/explorer_constants.js | 0 .../ml/public/{ => application}/explorer/explorer_controller.js | 0 .../{ => application}/explorer/explorer_dashboard_service.js | 0 .../explorer/explorer_react_wrapper_directive.js | 0 .../ml/public/{ => application}/explorer/explorer_swimlane.js | 0 .../public/{ => application}/explorer/explorer_swimlane.test.js | 0 .../{ => application}/explorer/explorer_swimlane.test.mocks.ts | 0 .../ml/public/{ => application}/explorer/explorer_utils.js | 0 .../legacy/plugins/ml/public/{ => application}/explorer/index.js | 0 .../plugins/ml/public/{ => application}/explorer/legacy_utils.js | 0 .../ml/public/{ => application}/explorer/select_limit/index.js | 0 .../{ => application}/explorer/select_limit/select_limit.js | 0 .../{ => application}/explorer/select_limit/select_limit.test.js | 0 .../explorer/select_limit/select_limit_service.js | 0 .../{ => application}/formatters/abbreviate_whole_number.test.ts | 0 .../{ => application}/formatters/abbreviate_whole_number.ts | 0 .../ml/public/{ => application}/formatters/format_value.test.ts | 0 .../ml/public/{ => application}/formatters/format_value.ts | 0 .../ml/public/{ => application}/formatters/kibana_field_format.ts | 0 .../formatters/metric_change_description.test.ts | 0 .../{ => application}/formatters/metric_change_description.ts | 0 .../public/{ => application}/formatters/number_as_ordinal.test.ts | 0 .../ml/public/{ => application}/formatters/number_as_ordinal.ts | 0 .../{ => application}/formatters/round_to_decimal_place.test.ts | 0 .../public/{ => application}/formatters/round_to_decimal_place.ts | 0 .../ml/public/{ => application}/hacks/toggle_app_link_in_nav.js | 0 .../legacy/plugins/ml/public/{ => application}/jobs/_index.scss | 0 .../plugins/ml/public/{ => application}/jobs/breadcrumbs.ts | 0 .../custom_url_editor/__snapshots__/editor.test.js.snap | 0 .../components/custom_url_editor/__snapshots__/list.test.tsx.snap | 0 .../jobs/components/custom_url_editor/_custom_url_editor.scss | 0 .../jobs/components/custom_url_editor/_index.scss | 0 .../jobs/components/custom_url_editor/constants.ts | 0 .../{ => application}/jobs/components/custom_url_editor/editor.js | 0 .../jobs/components/custom_url_editor/editor.test.js | 0 .../{ => application}/jobs/components/custom_url_editor/index.js | 0 .../jobs/components/custom_url_editor/list.test.tsx | 0 .../{ => application}/jobs/components/custom_url_editor/list.tsx | 0 .../jobs/components/custom_url_editor/utils.d.ts | 0 .../{ => application}/jobs/components/custom_url_editor/utils.js | 0 x-pack/legacy/plugins/ml/public/{ => application}/jobs/index.js | 0 .../ml/public/{ => application}/jobs/jobs_list/_index.scss | 0 .../ml/public/{ => application}/jobs/jobs_list/_jobs_list.scss | 0 .../components/create_watch_flyout/create_watch_flyout.js | 0 .../components/create_watch_flyout/create_watch_service.js | 0 .../jobs_list/components/create_watch_flyout/create_watch_view.js | 0 .../jobs/jobs_list/components/create_watch_flyout/email.html | 0 .../components/create_watch_flyout/email_influencers.html | 0 .../jobs/jobs_list/components/create_watch_flyout/index.js | 0 .../jobs/jobs_list/components/create_watch_flyout/watch.js | 0 .../jobs_list/components/delete_job_modal/delete_job_modal.js | 0 .../jobs/jobs_list/components/delete_job_modal/index.js | 0 .../jobs_list/components/edit_job_flyout/_edit_job_flyout.scss | 0 .../jobs/jobs_list/components/edit_job_flyout/_index.scss | 0 .../jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js | 0 .../jobs/jobs_list/components/edit_job_flyout/edit_utils.js | 0 .../jobs/jobs_list/components/edit_job_flyout/index.js | 0 .../jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js | 0 .../jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js | 0 .../jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js | 0 .../jobs/jobs_list/components/edit_job_flyout/tabs/index.js | 0 .../jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js | 0 .../jobs/jobs_list/components/job_actions/index.js | 0 .../jobs/jobs_list/components/job_actions/management.js | 0 .../jobs/jobs_list/components/job_actions/results.js | 0 .../jobs/jobs_list/components/job_details/_index.scss | 0 .../jobs/jobs_list/components/job_details/_job_details.scss | 0 .../jobs/jobs_list/components/job_details/datafeed_preview_tab.js | 0 .../jobs/jobs_list/components/job_details/extract_job_details.js | 0 .../components/job_details/forecasts_table/forecasts_table.js | 0 .../jobs_list/components/job_details/forecasts_table/index.js | 0 .../jobs/jobs_list/components/job_details/format_values.js | 0 .../jobs/jobs_list/components/job_details/index.js | 0 .../jobs/jobs_list/components/job_details/job_details.js | 0 .../jobs/jobs_list/components/job_details/job_details_pane.js | 0 .../jobs/jobs_list/components/job_details/job_messages_pane.tsx | 0 .../jobs/jobs_list/components/job_details/json_tab.js | 0 .../jobs/jobs_list/components/job_filter_bar/_index.scss | 0 .../jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss | 0 .../jobs/jobs_list/components/job_filter_bar/index.js | 0 .../jobs/jobs_list/components/job_filter_bar/job_filter_bar.js | 0 .../jobs/jobs_list/components/job_group/_index.scss | 0 .../jobs/jobs_list/components/job_group/_job_group.scss | 0 .../jobs/jobs_list/components/job_group/index.js | 0 .../jobs/jobs_list/components/job_group/job_group.js | 0 .../jobs/jobs_list/components/jobs_list/_index.scss | 0 .../jobs/jobs_list/components/jobs_list/_jobs_list.scss | 0 .../jobs/jobs_list/components/jobs_list/index.js | 0 .../jobs/jobs_list/components/jobs_list/job_description.js | 0 .../jobs/jobs_list/components/jobs_list/jobs_list.js | 0 .../jobs/jobs_list/components/jobs_list_view/_index.scss | 0 .../jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss | 0 .../jobs/jobs_list/components/jobs_list_view/index.js | 0 .../jobs/jobs_list/components/jobs_list_view/jobs_list_view.js | 0 .../jobs/jobs_list/components/jobs_stats_bar/index.js | 0 .../jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js | 0 .../jobs/jobs_list/components/ml_job_editor/index.ts | 0 .../jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts | 0 .../jobs/jobs_list/components/ml_job_editor/ml_job_editor.js | 0 .../jobs/jobs_list/components/multi_job_actions/_index.scss | 0 .../components/multi_job_actions/_multi_job_actions.scss | 0 .../jobs/jobs_list/components/multi_job_actions/actions_menu.js | 0 .../multi_job_actions/group_selector/_group_selector.scss | 0 .../components/multi_job_actions/group_selector/_index.scss | 0 .../multi_job_actions/group_selector/group_list/_group_list.scss | 0 .../multi_job_actions/group_selector/group_list/_index.scss | 0 .../multi_job_actions/group_selector/group_list/group_list.js | 0 .../multi_job_actions/group_selector/group_list/index.js | 0 .../components/multi_job_actions/group_selector/group_selector.js | 0 .../components/multi_job_actions/group_selector/index.js | 0 .../multi_job_actions/group_selector/new_group_input/_index.scss | 0 .../group_selector/new_group_input/_new_group_input.scss | 0 .../multi_job_actions/group_selector/new_group_input/index.js | 0 .../group_selector/new_group_input/new_group_input.js | 0 .../jobs/jobs_list/components/multi_job_actions/index.js | 0 .../jobs_list/components/multi_job_actions/multi_job_actions.js | 0 .../jobs/jobs_list/components/new_job_button/index.js | 0 .../jobs/jobs_list/components/new_job_button/new_job_button.js | 0 .../jobs/jobs_list/components/refresh_jobs_list_button/index.js | 0 .../refresh_jobs_list_button/refresh_jobs_list_button.js | 0 .../jobs/jobs_list/components/start_datafeed_modal/_index.scss | 0 .../jobs/jobs_list/components/start_datafeed_modal/index.js | 0 .../components/start_datafeed_modal/start_datafeed_modal.js | 0 .../start_datafeed_modal/time_range_selector/_index.scss | 0 .../time_range_selector/_time_range_selector.scss | 0 .../components/start_datafeed_modal/time_range_selector/index.js | 0 .../time_range_selector/time_range_selector.js | 0 .../public/{ => application}/jobs/jobs_list/components/utils.js | 0 .../{ => application}/jobs/jobs_list/components/validate_job.js | 0 .../ml/public/{ => application}/jobs/jobs_list/directive.js | 0 .../plugins/ml/public/{ => application}/jobs/jobs_list/index.js | 0 .../plugins/ml/public/{ => application}/jobs/jobs_list/jobs.js | 0 .../jobs/new_job/common/chart_loader/chart_loader.ts | 0 .../{ => application}/jobs/new_job/common/chart_loader/index.ts | 0 .../jobs/new_job/common/chart_loader/searches.ts | 0 .../{ => application}/jobs/new_job/common/components/index.ts | 0 .../jobs/new_job/common/components/job_groups_input.tsx | 0 .../jobs/new_job/common/components/time_range_picker.tsx | 0 .../ml/public/{ => application}/jobs/new_job/common/index.ts | 0 .../jobs/new_job/common/index_pattern_context.ts | 0 .../jobs/new_job/common/job_creator/advanced_job_creator.ts | 0 .../jobs/new_job/common/job_creator/configs/combined_job.ts | 0 .../jobs/new_job/common/job_creator/configs/datafeed.ts | 0 .../jobs/new_job/common/job_creator/configs/index.ts | 0 .../jobs/new_job/common/job_creator/configs/job.ts | 0 .../{ => application}/jobs/new_job/common/job_creator/index.ts | 0 .../jobs/new_job/common/job_creator/job_creator.ts | 0 .../jobs/new_job/common/job_creator/job_creator_factory.ts | 0 .../jobs/new_job/common/job_creator/multi_metric_job_creator.ts | 0 .../jobs/new_job/common/job_creator/population_job_creator.ts | 0 .../jobs/new_job/common/job_creator/single_metric_job_creator.ts | 0 .../jobs/new_job/common/job_creator/type_guards.ts | 0 .../jobs/new_job/common/job_creator/util/constants.ts | 0 .../jobs/new_job/common/job_creator/util/default_configs.ts | 0 .../jobs/new_job/common/job_creator/util/general.ts | 0 .../{ => application}/jobs/new_job/common/job_runner/index.ts | 0 .../jobs/new_job/common/job_runner/job_runner.ts | 0 .../{ => application}/jobs/new_job/common/job_validator/index.ts | 0 .../jobs/new_job/common/job_validator/job_validator.ts | 0 .../{ => application}/jobs/new_job/common/job_validator/util.ts | 0 .../{ => application}/jobs/new_job/common/results_loader/index.ts | 0 .../jobs/new_job/common/results_loader/results_loader.ts | 0 .../jobs/new_job/common/results_loader/searches.ts | 0 .../plugins/ml/public/{ => application}/jobs/new_job/index.ts | 0 .../new_job/pages/components/charts/anomaly_chart/anomalies.tsx | 0 .../pages/components/charts/anomaly_chart/anomaly_chart.tsx | 0 .../jobs/new_job/pages/components/charts/anomaly_chart/index.ts | 0 .../jobs/new_job/pages/components/charts/anomaly_chart/line.tsx | 0 .../pages/components/charts/anomaly_chart/model_bounds.tsx | 0 .../new_job/pages/components/charts/anomaly_chart/scatter.tsx | 0 .../jobs/new_job/pages/components/charts/common/axes.tsx | 0 .../jobs/new_job/pages/components/charts/common/settings.ts | 0 .../jobs/new_job/pages/components/charts/common/utils.ts | 0 .../pages/components/charts/event_rate_chart/event_rate_chart.tsx | 0 .../new_job/pages/components/charts/event_rate_chart/index.ts | 0 .../jobs/new_job/pages/components/charts/loading_wrapper/index.ts | 0 .../pages/components/charts/loading_wrapper/loading_wrapper.tsx | 0 .../common/datafeed_preview_flyout/datafeed_preview_flyout.tsx | 0 .../pages/components/common/datafeed_preview_flyout/index.ts | 0 .../new_job/pages/components/common/json_editor_flyout/index.ts | 0 .../components/common/json_editor_flyout/json_editor_flyout.tsx | 0 .../pages/components/common/model_memory_limit/description.tsx | 0 .../new_job/pages/components/common/model_memory_limit/index.ts | 0 .../common/model_memory_limit/model_memory_limit_input.tsx | 0 .../components/datafeed_step/components/frequency/description.tsx | 0 .../datafeed_step/components/frequency/frequency_input.tsx | 0 .../pages/components/datafeed_step/components/frequency/index.ts | 0 .../new_job/pages/components/datafeed_step/components/hooks.ts | 0 .../components/datafeed_step/components/query/description.tsx | 0 .../pages/components/datafeed_step/components/query/index.ts | 0 .../components/datafeed_step/components/query/query_input.tsx | 0 .../datafeed_step/components/query_delay/description.tsx | 0 .../components/datafeed_step/components/query_delay/index.ts | 0 .../datafeed_step/components/query_delay/query_delay_input.tsx | 0 .../datafeed_step/components/scroll_size/description.tsx | 0 .../components/datafeed_step/components/scroll_size/index.ts | 0 .../datafeed_step/components/scroll_size/scroll_size_input.tsx | 0 .../datafeed_step/components/time_field/description.tsx | 0 .../pages/components/datafeed_step/components/time_field/index.ts | 0 .../components/datafeed_step/components/time_field/time_field.tsx | 0 .../datafeed_step/components/time_field/time_field_select.tsx | 0 .../jobs/new_job/pages/components/datafeed_step/datafeed.tsx | 0 .../jobs/new_job/pages/components/datafeed_step/index.ts | 0 .../jobs/new_job/pages/components/job_creator_context.ts | 0 .../components/additional_section/additional_section.tsx | 0 .../components/calendars/calendars_selection.tsx | 0 .../additional_section/components/calendars/description.tsx | 0 .../components/additional_section/components/calendars/index.ts | 0 .../job_details_step/components/additional_section/index.ts | 0 .../components/advanced_section/advanced_section.tsx | 0 .../components/dedicated_index/dedicated_index_switch.tsx | 0 .../advanced_section/components/dedicated_index/description.tsx | 0 .../advanced_section/components/dedicated_index/index.ts | 0 .../advanced_section/components/model_plot/description.tsx | 0 .../components/advanced_section/components/model_plot/index.ts | 0 .../advanced_section/components/model_plot/model_plot_switch.tsx | 0 .../job_details_step/components/advanced_section/index.ts | 0 .../components/job_details_step/components/groups/description.tsx | 0 .../job_details_step/components/groups/groups_input.tsx | 0 .../pages/components/job_details_step/components/groups/index.ts | 0 .../job_details_step/components/job_description/description.tsx | 0 .../job_details_step/components/job_description/index.ts | 0 .../components/job_description/job_description_input.tsx | 0 .../components/job_details_step/components/job_id/description.tsx | 0 .../pages/components/job_details_step/components/job_id/index.ts | 0 .../job_details_step/components/job_id/job_id_input.tsx | 0 .../jobs/new_job/pages/components/job_details_step/index.ts | 0 .../new_job/pages/components/job_details_step/job_details.tsx | 0 .../advanced_detector_modal/advanced_detector_modal.tsx | 0 .../components/advanced_detector_modal/descriptions.tsx | 0 .../pick_fields_step/components/advanced_detector_modal/index.tsx | 0 .../components/advanced_detector_modal/modal_wrapper.tsx | 0 .../pick_fields_step/components/advanced_view/advanced_view.tsx | 0 .../pick_fields_step/components/advanced_view/detector_list.tsx | 0 .../pick_fields_step/components/advanced_view/extra.tsx | 0 .../components/pick_fields_step/components/advanced_view/index.ts | 0 .../components/advanced_view/metric_selection.tsx | 0 .../components/advanced_view/metric_selection_summary.tsx | 0 .../pick_fields_step/components/advanced_view/metric_selector.tsx | 0 .../pick_fields_step/components/advanced_view/settings.tsx | 0 .../pick_fields_step/components/agg_select/agg_select.tsx | 0 .../components/pick_fields_step/components/agg_select/index.ts | 0 .../pick_fields_step/components/bucket_span/bucket_span.tsx | 0 .../pick_fields_step/components/bucket_span/bucket_span_input.tsx | 0 .../pick_fields_step/components/bucket_span/description.tsx | 0 .../components/pick_fields_step/components/bucket_span/index.ts | 0 .../components/bucket_span_estimator/bucket_span_estimator.tsx | 0 .../components/bucket_span_estimator/estimate_bucket_span.ts | 0 .../pick_fields_step/components/bucket_span_estimator/index.ts | 0 .../components/categorization_field/categorization_field.tsx | 0 .../categorization_field/categorization_field_select.tsx | 0 .../components/categorization_field/description.tsx | 0 .../pick_fields_step/components/categorization_field/index.ts | 0 .../pick_fields_step/components/detector_title/detector_title.tsx | 0 .../pick_fields_step/components/detector_title/index.ts | 0 .../pick_fields_step/components/influencers/description.tsx | 0 .../components/pick_fields_step/components/influencers/index.ts | 0 .../pick_fields_step/components/influencers/influencers.tsx | 0 .../components/influencers/influencers_select.tsx | 0 .../pick_fields_step/components/multi_metric_view/chart_grid.tsx | 0 .../pick_fields_step/components/multi_metric_view/index.ts | 0 .../components/multi_metric_view/metric_selection.tsx | 0 .../components/multi_metric_view/metric_selection_summary.tsx | 0 .../components/multi_metric_view/metric_selector.tsx | 0 .../components/multi_metric_view/multi_metric_view.tsx | 0 .../pick_fields_step/components/multi_metric_view/settings.tsx | 0 .../pick_fields_step/components/population_view/chart_grid.tsx | 0 .../pick_fields_step/components/population_view/index.ts | 0 .../components/population_view/metric_selection.tsx | 0 .../components/population_view/metric_selection_summary.tsx | 0 .../components/population_view/metric_selector.tsx | 0 .../components/population_view/population_view.tsx | 0 .../pick_fields_step/components/population_view/settings.tsx | 0 .../pick_fields_step/components/single_metric_view/index.ts | 0 .../components/single_metric_view/metric_selection.tsx | 0 .../components/single_metric_view/metric_selection_summary.tsx | 0 .../pick_fields_step/components/single_metric_view/settings.tsx | 0 .../components/single_metric_view/single_metric_view.tsx | 0 .../pick_fields_step/components/sparse_data/description.tsx | 0 .../components/pick_fields_step/components/sparse_data/index.ts | 0 .../components/sparse_data/sparse_data_switch.tsx | 0 .../pick_fields_step/components/split_cards/animate_split_hook.ts | 0 .../components/pick_fields_step/components/split_cards/index.ts | 0 .../pick_fields_step/components/split_cards/split_cards.tsx | 0 .../pick_fields_step/components/split_field/by_field.tsx | 0 .../pick_fields_step/components/split_field/description.tsx | 0 .../components/pick_fields_step/components/split_field/index.ts | 0 .../pick_fields_step/components/split_field/split_field.tsx | 0 .../components/split_field/split_field_select.tsx | 0 .../components/summary_count_field/description.tsx | 0 .../pick_fields_step/components/summary_count_field/index.ts | 0 .../components/summary_count_field/summary_count_field.tsx | 0 .../components/summary_count_field/summary_count_field_select.tsx | 0 .../jobs/new_job/pages/components/pick_fields_step/index.ts | 0 .../new_job/pages/components/pick_fields_step/pick_fields.tsx | 0 .../{ => application}/jobs/new_job/pages/components/step_types.ts | 0 .../new_job/pages/components/summary_step/components/common.tsx | 0 .../summary_step/components/datafeed_details/datafeed_details.tsx | 0 .../components/summary_step/components/datafeed_details/index.ts | 0 .../summary_step/components/detector_chart/detector_chart.tsx | 0 .../components/summary_step/components/detector_chart/index.ts | 0 .../pages/components/summary_step/components/job_details/index.ts | 0 .../summary_step/components/job_details/job_details.tsx | 0 .../components/summary_step/components/job_progress/index.ts | 0 .../summary_step/components/job_progress/job_progress.tsx | 0 .../components/summary_step/components/post_save_options/index.ts | 0 .../components/post_save_options/post_save_options.tsx | 0 .../jobs/new_job/pages/components/summary_step/index.ts | 0 .../jobs/new_job/pages/components/summary_step/summary.tsx | 0 .../jobs/new_job/pages/components/time_range_step/index.ts | 0 .../jobs/new_job/pages/components/time_range_step/time_range.tsx | 0 .../jobs/new_job/pages/components/validation_step/index.ts | 0 .../jobs/new_job/pages/components/validation_step/validation.tsx | 0 .../jobs/new_job/pages/components/wizard_nav/index.ts | 0 .../jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx | 0 .../jobs/new_job/pages/index_or_search/__test__/directive.js | 0 .../jobs/new_job/pages/index_or_search/directive.tsx | 0 .../{ => application}/jobs/new_job/pages/index_or_search/page.tsx | 0 .../new_job/pages/index_or_search/preconfigured_job_redirect.ts | 0 .../{ => application}/jobs/new_job/pages/index_or_search/route.ts | 0 .../jobs/new_job/pages/job_type/__test__/directive.js | 0 .../{ => application}/jobs/new_job/pages/job_type/directive.tsx | 0 .../public/{ => application}/jobs/new_job/pages/job_type/page.tsx | 0 .../public/{ => application}/jobs/new_job/pages/job_type/route.ts | 0 .../{ => application}/jobs/new_job/pages/new_job/directive.tsx | 0 .../public/{ => application}/jobs/new_job/pages/new_job/page.tsx | 0 .../public/{ => application}/jobs/new_job/pages/new_job/route.ts | 0 .../{ => application}/jobs/new_job/pages/new_job/wizard.tsx | 0 .../jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx | 0 .../{ => application}/jobs/new_job/pages/new_job/wizard_steps.tsx | 0 .../jobs/new_job/recognize/__test__/directive.js | 0 .../jobs/new_job/recognize/components/create_result_callout.tsx | 0 .../jobs/new_job/recognize/components/edit_job.tsx | 0 .../jobs/new_job/recognize/components/job_item.tsx | 0 .../jobs/new_job/recognize/components/job_settings_form.tsx | 0 .../jobs/new_job/recognize/components/kibana_objects.tsx | 0 .../jobs/new_job/recognize/components/module_jobs.tsx | 0 .../public/{ => application}/jobs/new_job/recognize/directive.tsx | 0 .../ml/public/{ => application}/jobs/new_job/recognize/page.tsx | 0 .../public/{ => application}/jobs/new_job/recognize/resolvers.ts | 0 .../ml/public/{ => application}/jobs/new_job/recognize/route.ts | 0 .../public/{ => application}/jobs/new_job/utils/new_job_utils.ts | 0 .../public/{ => application}/license/__tests__/check_license.js | 0 .../plugins/ml/public/{ => application}/license/check_license.tsx | 0 .../plugins/ml/public/{ => application}/management/_index.scss | 0 .../plugins/ml/public/{ => application}/management/breadcrumbs.ts | 0 .../plugins/ml/public/{ => application}/management/index.ts | 0 .../ml/public/{ => application}/management/jobs_list/_index.scss | 0 .../{ => application}/management/jobs_list/components/_index.scss | 0 .../management/jobs_list/components/access_denied_page.tsx | 0 .../{ => application}/management/jobs_list/components/index.ts | 0 .../jobs_list/components/jobs_list_page/_analytics_table.scss | 0 .../management/jobs_list/components/jobs_list_page/_buttons.scss | 0 .../jobs_list/components/jobs_list_page/_expanded_row.scss | 0 .../jobs_list/components/jobs_list_page/_stats_bar.scss | 0 .../management/jobs_list/components/jobs_list_page/index.ts | 0 .../jobs_list/components/jobs_list_page/jobs_list_page.tsx | 0 .../ml/public/{ => application}/management/jobs_list/index.ts | 0 .../ml/public/{ => application}/management/management_urls.ts | 0 x-pack/legacy/plugins/ml/public/{ => application}/ml.svg | 0 .../ml/public/{ => application}/ml_nodes_check/check_ml_nodes.ts | 0 .../plugins/ml/public/{ => application}/ml_nodes_check/index.ts | 0 .../plugins/ml/public/{ => application}/overview/_index.scss | 0 .../plugins/ml/public/{ => application}/overview/breadcrumbs.ts | 0 .../ml/public/{ => application}/overview/components/_index.scss | 0 .../overview/components/analytics_panel/analytics_panel.tsx | 0 .../overview/components/analytics_panel/index.ts | 0 .../overview/components/analytics_panel/table.tsx | 0 .../overview/components/anomaly_detection_panel/actions.tsx | 0 .../anomaly_detection_panel/anomaly_detection_panel.tsx | 0 .../overview/components/anomaly_detection_panel/index.ts | 0 .../overview/components/anomaly_detection_panel/table.tsx | 0 .../overview/components/anomaly_detection_panel/utils.ts | 0 .../ml/public/{ => application}/overview/components/content.tsx | 0 .../ml/public/{ => application}/overview/components/sidebar.tsx | 0 .../plugins/ml/public/{ => application}/overview/directive.tsx | 0 .../legacy/plugins/ml/public/{ => application}/overview/index.ts | 0 .../ml/public/{ => application}/overview/overview_page.tsx | 0 .../legacy/plugins/ml/public/{ => application}/overview/route.ts | 0 .../ml/public/{ => application}/privilege/check_privilege.ts | 0 .../ml/public/{ => application}/privilege/get_privileges.ts | 0 .../services/__mocks__/cloudwatch_job_caps_response.json | 0 .../{ => application}/services/__mocks__/ml_info_response.json | 0 .../{ => application}/services/annotations_service.test.tsx | 0 .../ml/public/{ => application}/services/annotations_service.tsx | 0 .../ml/public/{ => application}/services/calendar_service.js | 0 .../ml/public/{ => application}/services/field_format_service.ts | 0 .../ml/public/{ => application}/services/forecast_service.js | 0 .../plugins/ml/public/{ => application}/services/http_service.js | 0 .../ml/public/{ => application}/services/job_messages_service.js | 0 .../plugins/ml/public/{ => application}/services/job_service.d.ts | 0 .../plugins/ml/public/{ => application}/services/job_service.js | 0 .../ml/public/{ => application}/services/mapping_service.js | 0 .../{ => application}/services/ml_api_service/annotations.js | 0 .../services/ml_api_service/data_frame_analytics.js | 0 .../{ => application}/services/ml_api_service/datavisualizer.js | 0 .../public/{ => application}/services/ml_api_service/filters.js | 0 .../public/{ => application}/services/ml_api_service/index.d.ts | 0 .../ml/public/{ => application}/services/ml_api_service/index.js | 0 .../ml/public/{ => application}/services/ml_api_service/jobs.js | 0 .../public/{ => application}/services/ml_api_service/results.js | 0 .../ml/public/{ => application}/services/ml_server_info.test.ts | 0 .../ml/public/{ => application}/services/ml_server_info.ts | 0 .../services/new_job_capabilities._service.test.ts | 0 .../{ => application}/services/new_job_capabilities_service.ts | 0 .../ml/public/{ => application}/services/results_service.d.ts | 0 .../ml/public/{ => application}/services/results_service.js | 0 .../plugins/ml/public/{ => application}/services/table_service.js | 0 .../{ => application}/services/timefilter_refresh_service.tsx | 0 .../ml/public/{ => application}/services/upgrade_service.ts | 0 .../plugins/ml/public/{ => application}/settings/_index.scss | 0 .../plugins/ml/public/{ => application}/settings/_settings.scss | 0 .../plugins/ml/public/{ => application}/settings/breadcrumbs.ts | 0 .../ml/public/{ => application}/settings/calendars/_index.scss | 0 .../calendars/edit/__snapshots__/new_calendar.test.js.snap | 0 .../public/{ => application}/settings/calendars/edit/_edit.scss | 0 .../public/{ => application}/settings/calendars/edit/_index.scss | 0 .../edit/calendar_form/__snapshots__/calendar_form.test.js.snap | 0 .../settings/calendars/edit/calendar_form/calendar_form.js | 0 .../settings/calendars/edit/calendar_form/calendar_form.test.js | 0 .../settings/calendars/edit/calendar_form/index.js | 0 .../{ => application}/settings/calendars/edit/directive.tsx | 0 .../edit/events_table/__snapshots__/events_table.test.js.snap | 0 .../settings/calendars/edit/events_table/events_table.js | 0 .../settings/calendars/edit/events_table/events_table.test.js | 0 .../settings/calendars/edit/events_table/index.js | 0 .../edit/import_modal/__snapshots__/import_modal.test.js.snap | 0 .../settings/calendars/edit/import_modal/import_modal.js | 0 .../settings/calendars/edit/import_modal/import_modal.test.js | 0 .../settings/calendars/edit/import_modal/index.js | 0 .../settings/calendars/edit/import_modal/utils.js | 0 .../imported_events/__snapshots__/imported_events.test.js.snap | 0 .../settings/calendars/edit/imported_events/imported_events.js | 0 .../calendars/edit/imported_events/imported_events.test.js | 0 .../settings/calendars/edit/imported_events/index.js | 0 .../ml/public/{ => application}/settings/calendars/edit/index.ts | 0 .../{ => application}/settings/calendars/edit/new_calendar.d.ts | 0 .../{ => application}/settings/calendars/edit/new_calendar.js | 0 .../settings/calendars/edit/new_calendar.test.js | 0 .../settings/calendars/edit/new_event_modal/index.js | 0 .../settings/calendars/edit/new_event_modal/new_event_modal.js | 0 .../calendars/edit/new_event_modal/new_event_modal.test.js | 0 .../ml/public/{ => application}/settings/calendars/edit/utils.js | 0 .../ml/public/{ => application}/settings/calendars/index.js | 0 .../calendars/list/__snapshots__/calendars_list.test.js.snap | 0 .../settings/calendars/list/__snapshots__/header.test.js.snap | 0 .../public/{ => application}/settings/calendars/list/_index.scss | 0 .../public/{ => application}/settings/calendars/list/_list.scss | 0 .../{ => application}/settings/calendars/list/calendars_list.d.ts | 0 .../{ => application}/settings/calendars/list/calendars_list.js | 0 .../settings/calendars/list/calendars_list.test.js | 0 .../{ => application}/settings/calendars/list/delete_calendars.js | 0 .../{ => application}/settings/calendars/list/directive.tsx | 0 .../ml/public/{ => application}/settings/calendars/list/header.js | 0 .../{ => application}/settings/calendars/list/header.test.js | 0 .../ml/public/{ => application}/settings/calendars/list/index.ts | 0 .../calendars/list/table/__snapshots__/table.test.js.snap | 0 .../{ => application}/settings/calendars/list/table/index.js | 0 .../{ => application}/settings/calendars/list/table/table.js | 0 .../{ => application}/settings/calendars/list/table/table.test.js | 0 .../{ => application}/settings/filter_lists/_filter_lists.scss | 0 .../ml/public/{ => application}/settings/filter_lists/_index.scss | 0 .../add_item_popover/__snapshots__/add_item_popover.test.js.snap | 0 .../filter_lists/components/add_item_popover/add_item_popover.js | 0 .../components/add_item_popover/add_item_popover.test.js | 0 .../settings/filter_lists/components/add_item_popover/index.js | 0 .../__snapshots__/delete_filter_list_modal.test.js.snap | 0 .../delete_filter_list_modal/delete_filter_list_modal.js | 0 .../delete_filter_list_modal/delete_filter_list_modal.test.js | 0 .../components/delete_filter_list_modal/delete_filter_lists.js | 0 .../filter_lists/components/delete_filter_list_modal/index.js | 0 .../__snapshots__/edit_description_popover.test.js.snap | 0 .../edit_description_popover/edit_description_popover.js | 0 .../edit_description_popover/edit_description_popover.test.js | 0 .../filter_lists/components/edit_description_popover/index.js | 0 .../__snapshots__/filter_list_usage_popover.test.js.snap | 0 .../filter_list_usage_popover/filter_list_usage_popover.js | 0 .../filter_list_usage_popover/filter_list_usage_popover.test.js | 0 .../filter_lists/components/filter_list_usage_popover/index.js | 0 .../filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap | 0 .../settings/filter_lists/edit/__snapshots__/header.test.js.snap | 0 .../settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap | 0 .../{ => application}/settings/filter_lists/edit/_edit.scss | 0 .../{ => application}/settings/filter_lists/edit/_index.scss | 0 .../{ => application}/settings/filter_lists/edit/directive.tsx | 0 .../settings/filter_lists/edit/edit_filter_list.d.ts | 0 .../settings/filter_lists/edit/edit_filter_list.js | 0 .../settings/filter_lists/edit/edit_filter_list.test.js | 0 .../public/{ => application}/settings/filter_lists/edit/header.js | 0 .../{ => application}/settings/filter_lists/edit/header.test.js | 0 .../public/{ => application}/settings/filter_lists/edit/index.ts | 0 .../{ => application}/settings/filter_lists/edit/toolbar.js | 0 .../{ => application}/settings/filter_lists/edit/toolbar.test.js | 0 .../public/{ => application}/settings/filter_lists/edit/utils.js | 0 .../ml/public/{ => application}/settings/filter_lists/index.ts | 0 .../filter_lists/list/__snapshots__/filter_lists.test.js.snap | 0 .../settings/filter_lists/list/__snapshots__/header.test.js.snap | 0 .../settings/filter_lists/list/__snapshots__/table.test.js.snap | 0 .../{ => application}/settings/filter_lists/list/directive.tsx | 0 .../settings/filter_lists/list/filter_lists.d.ts | 0 .../{ => application}/settings/filter_lists/list/filter_lists.js | 0 .../settings/filter_lists/list/filter_lists.test.js | 0 .../public/{ => application}/settings/filter_lists/list/header.js | 0 .../{ => application}/settings/filter_lists/list/header.test.js | 0 .../public/{ => application}/settings/filter_lists/list/index.ts | 0 .../public/{ => application}/settings/filter_lists/list/table.js | 0 .../{ => application}/settings/filter_lists/list/table.test.js | 0 .../legacy/plugins/ml/public/{ => application}/settings/index.ts | 0 .../plugins/ml/public/{ => application}/settings/settings.test.js | 0 .../plugins/ml/public/{ => application}/settings/settings.tsx | 0 .../ml/public/{ => application}/settings/settings_directive.tsx | 0 .../timeseriesexplorer/__tests__/timeseriesexplorer_directive.js | 0 .../ml/public/{ => application}/timeseriesexplorer/_index.scss | 0 .../{ => application}/timeseriesexplorer/_timeseriesexplorer.scss | 0 .../timeseriesexplorer/_timeseriesexplorer_annotations.scss | 0 .../ml/public/{ => application}/timeseriesexplorer/breadcrumbs.js | 0 .../components/context_chart_mask/context_chart_mask.js | 0 .../timeseriesexplorer/components/context_chart_mask/index.js | 0 .../components/entity_control/entity_control.js | 0 .../timeseriesexplorer/components/entity_control/index.js | 0 .../components/forecasting_modal/forecast_progress.js | 0 .../components/forecasting_modal/forecasting_modal.js | 0 .../components/forecasting_modal/forecasts_list.js | 0 .../timeseriesexplorer/components/forecasting_modal/index.js | 0 .../timeseriesexplorer/components/forecasting_modal/modal.js | 0 .../components/forecasting_modal/progress_icon.js | 0 .../components/forecasting_modal/progress_states.js | 0 .../components/forecasting_modal/run_controls.js | 0 .../timeseries_chart/__mocks__/mock_annotations_overlap.json | 0 .../components/timeseries_chart/timeseries_chart.d.ts | 0 .../components/timeseries_chart/timeseries_chart.js | 0 .../components/timeseries_chart/timeseries_chart.test.js | 0 .../components/timeseries_chart/timeseries_chart.test.mocks.ts | 0 .../timeseries_chart/timeseries_chart_annotations.test.ts | 0 .../components/timeseries_chart/timeseries_chart_annotations.ts | 0 .../components/timeseriesexplorer_no_chart_data/index.js | 0 .../timeseriesexplorer_no_chart_data.js | 0 .../components/timeseriesexplorer_no_jobs_found/index.js | 0 .../timeseriesexplorer_no_jobs_found.js | 0 .../ml/public/{ => application}/timeseriesexplorer/index.js | 0 .../timeseriesexplorer/timeseries_search_service.js | 0 .../{ => application}/timeseriesexplorer/timeseriesexplorer.js | 0 .../timeseriesexplorer/timeseriesexplorer_constants.js | 0 .../timeseriesexplorer/timeseriesexplorer_directive.js | 0 .../timeseriesexplorer/timeseriesexplorer_route.js | 0 .../timeseriesexplorer/timeseriesexplorer_utils.js | 0 .../util/__snapshots__/observable_utils.test.tsx.snap | 0 .../ml/public/{ => application}/util/__tests__/app_state_utils.js | 0 .../public/{ => application}/util/__tests__/calc_auto_interval.js | 0 .../ml/public/{ => application}/util/__tests__/chart_utils.js | 0 .../ml/public/{ => application}/util/__tests__/ml_time_buckets.js | 0 .../ml/public/{ => application}/util/__tests__/string_utils.js | 0 .../plugins/ml/public/{ => application}/util/app_state_utils.js | 0 .../ml/public/{ => application}/util/calc_auto_interval.js | 0 .../ml/public/{ => application}/util/chart_config_builder.js | 0 .../plugins/ml/public/{ => application}/util/chart_utils.js | 0 .../plugins/ml/public/{ => application}/util/chart_utils.test.js | 0 .../ml/public/{ => application}/util/custom_url_utils.test.ts | 0 .../plugins/ml/public/{ => application}/util/custom_url_utils.ts | 0 .../plugins/ml/public/{ => application}/util/date_utils.test.ts | 0 .../legacy/plugins/ml/public/{ => application}/util/date_utils.ts | 0 .../ml/public/{ => application}/util/field_types_utils.test.ts | 0 .../plugins/ml/public/{ => application}/util/field_types_utils.ts | 0 .../plugins/ml/public/{ => application}/util/index_utils.ts | 0 .../legacy/plugins/ml/public/{ => application}/util/inherits.js | 0 .../legacy/plugins/ml/public/{ => application}/util/ml_error.js | 0 .../plugins/ml/public/{ => application}/util/object_utils.test.ts | 0 .../plugins/ml/public/{ => application}/util/object_utils.ts | 0 .../ml/public/{ => application}/util/observable_utils.test.tsx | 0 .../plugins/ml/public/{ => application}/util/observable_utils.tsx | 0 .../plugins/ml/public/{ => application}/util/recently_accessed.ts | 0 .../plugins/ml/public/{ => application}/util/string_utils.d.ts | 0 .../plugins/ml/public/{ => application}/util/string_utils.js | 0 .../legacy/plugins/ml/public/{ => application}/util/test_utils.ts | 0 .../plugins/ml/public/{ => application}/util/time_buckets.d.ts | 0 .../plugins/ml/public/{ => application}/util/time_buckets.js | 0 .../plugins/ml/public/{ => application}/util/url_utils.test.ts | 0 .../legacy/plugins/ml/public/{ => application}/util/url_utils.ts | 0 1102 files changed, 0 insertions(+), 0 deletions(-) rename x-pack/legacy/plugins/ml/public/{ => application}/_app.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/_hacks.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/_variables.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/access_denied/index.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/access_denied/page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/app.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotation_description_list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotation_description_list/index.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotation_description_list/index.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotation_flyout/index.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotation_flyout/index.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotations_table/__mocks__/mock_annotations.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotations_table/annotations_table.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotations_table/annotations_table.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotations_table/annotations_table.test.mocks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/annotations_table/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/annotations/delete_annotation_modal/index.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/_anomalies_table.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/anomalies_table.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/anomalies_table.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/anomalies_table_columns.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/anomalies_table_constants.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/anomaly_details.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/anomaly_details.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/description_cell.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/detector_cell.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/influencers_cell.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/links_menu.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/severity_cell/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/severity_cell/severity_cell.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/anomalies_table/severity_cell/severity_cell.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/chart_tooltip/_chart_tooltip.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/chart_tooltip/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/chart_tooltip/chart_tooltip.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/chart_tooltip/chart_tooltip_service.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/chart_tooltip/chart_tooltip_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/chart_tooltip/chart_tooltip_service.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/chart_tooltip/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/_controls.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/checkbox_showcharts/checkbox_showcharts.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/checkbox_showcharts/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/select_interval/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/select_interval/select_interval.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/select_interval/select_interval.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/select_severity/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/select_severity/_select_severity.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/select_severity/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/select_severity/select_severity.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/controls/select_severity/select_severity.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/create_job_link_card/create_job_link_card.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/create_job_link_card/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/custom_hooks/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/custom_hooks/use_partial_state.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/data_recognizer/data_recognizer.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/data_recognizer/data_recognizer.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/data_recognizer/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/data_recognizer/recognized_result.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/display_value/display_value.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/display_value/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/entity_cell/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/entity_cell/entity_cell.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/entity_cell/entity_cell.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/entity_cell/entity_cell.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/entity_cell/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_title_bar/_field_title_bar.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_title_bar/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_title_bar/field_title_bar.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_title_bar/field_title_bar.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_title_bar/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_type_icon/_field_type_icon.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_type_icon/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_type_icon/field_type_icon.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_type_icon/field_type_icon.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/field_type_icon/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/full_time_range_selector/full_time_range_selector.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/full_time_range_selector/full_time_range_selector.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/full_time_range_selector/full_time_range_selector_service.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/full_time_range_selector/index.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/influencers_list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/influencers_list/_influencers_list.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/influencers_list/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/influencers_list/influencers_list.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/items_grid/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/items_grid/_items_grid.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/items_grid/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/items_grid/items_grid.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/items_grid/items_grid_pagination.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_message_icon/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_message_icon/job_message_icon.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_messages/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_messages/job_messages.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/_job_selector.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/custom_selection_table/custom_selection_table.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/custom_selection_table/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/id_badges/id_badges.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/id_badges/id_badges.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/id_badges/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/job_select_service_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/job_selector.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/job_selector_badge/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/job_selector_badge/job_selector_badge.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/job_selector_table/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/job_selector_table/job_selector_table.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/job_selector_table/job_selector_table.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/new_selection_id_badges/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/new_selection_id_badges/new_selection_id_badges.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/timerange_bar/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/timerange_bar/timerange_bar.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/job_selector/timerange_bar/timerange_bar.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/__tests__/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/click_outside/click_outside.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/click_outside/click_outside.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/click_outside/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/filter_bar/filter_bar.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/filter_bar/filter_bar.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/filter_bar/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/kql_filter_bar.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/kql_filter_bar.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/suggestion/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/suggestion/suggestion.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/suggestion/suggestion.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/suggestions/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/suggestions/suggestions.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/suggestions/suggestions.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/kql_filter_bar/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/loading_indicator/__tests__/loading_indicator_directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/loading_indicator/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/loading_indicator/_loading_indicator.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/loading_indicator/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/loading_indicator/loading_indicator.html (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/loading_indicator/loading_indicator.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/loading_indicator/loading_indicator_directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/loading_indicator/loading_indicator_wrapper.html (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/message_call_out/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/message_call_out/message_call_out.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/messagebar/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/messagebar/messagebar_service.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/messagebar/messagebar_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/ml_in_memory_table/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/ml_in_memory_table/ml_in_memory_table.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/ml_in_memory_table/types.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/_navigation_menu.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/main_tabs.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/navigation_menu.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/tabs.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/tabs.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/top_nav/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/top_nav/top_nav.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/navigation_menu/top_nav/top_nav.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/node_available_warning/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/node_available_warning/node_available_warning.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/__snapshots__/actions_section.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/__snapshots__/condition_expression.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/__snapshots__/conditions_section.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/__snapshots__/scope_expression.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/__snapshots__/scope_section.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/__tests__/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/_rule_editor.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/actions_section.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/actions_section.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/components/detector_description_list/_detector_description_list.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/components/detector_description_list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/components/detector_description_list/detector_description_list.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/components/detector_description_list/detector_description_list.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/components/detector_description_list/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/condition_expression.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/condition_expression.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/conditions_section.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/conditions_section.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/rule_editor_flyout.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/rule_editor_flyout.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/scope_expression.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/scope_expression.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/scope_section.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/scope_section.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/add_to_filter_list_link.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/delete_rule_modal.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/delete_rule_modal.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/edit_condition_link.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/edit_condition_link.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/rule_action_panel.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/rule_action_panel.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/select_rule_action/select_rule_action.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/rule_editor/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/stats_bar/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/stats_bar/_stat.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/stats_bar/_stats_bar.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/stats_bar/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/stats_bar/stat.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/stats_bar/stats_bar.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/upgrade/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/upgrade/upgrade_warning.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/validate_job/__snapshots__/validate_job_view.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/validate_job/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/validate_job/validate_job_view.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/validate_job/validate_job_view.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/components/validate_job/validate_job_view.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/__mocks__/index_pattern.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/__mocks__/index_patterns.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/__mocks__/kibana_config.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/__mocks__/kibana_context_value.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/__mocks__/saved_search.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/kibana_context.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/use_current_index_pattern.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/use_current_saved_search.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/kibana/use_kibana_context.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/ui/__mocks__/mocks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/ui/__mocks__/use_ui_chrome_context.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/ui/__mocks__/use_ui_context.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/ui/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/ui/ui_context.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/ui/use_ui_chrome_context.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/contexts/ui/use_ui_context.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/common/analytics.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/common/analytics.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/common/fields.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/common/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_exploration/route.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/route.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/datavisualizer_selector.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/_file_datavisualizer.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/about_panel/_about_panel.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/about_panel/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/about_panel/about_panel.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/about_panel/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/about_panel/welcome_content.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/analysis_summary/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/analysis_summary/analysis_summary.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/analysis_summary/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/bottom_bar/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/edit_flyout.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/options/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/options/option_lists.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/options/options.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/overrides.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/overrides.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/edit_flyout/overrides_validation.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/experimental_badge/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/experimental_badge/experimental_badge.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/experimental_badge/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/fields_stats/_fields_stats.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/fields_stats/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/fields_stats/field_stats_card.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/fields_stats/fields_stats.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/fields_stats/get_field_names.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/fields_stats/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_contents/_file_contents.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_contents/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_contents/file_contents.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_contents/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/file_datavisualizer_view/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_errors/errors.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_errors/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_progress/import_progress.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_progress/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_settings/advanced.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_settings/import_settings.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_settings/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_settings/simple.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_summary/_import_sumary.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_summary/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_summary/import_summary.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_summary/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_view/import_view.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_view/importer/csv_importer.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_view/importer/importer.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_view/importer/importer_factory.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_view/importer/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_view/importer/sst_importer.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/import_view/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/results_links/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/results_links/results_links.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/results_view/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/results_view/_results_view.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/results_view/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/results_view/results_view.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/utils/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/utils/overrides.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/components/utils/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/file_datavisualizer.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/file_datavisualizer_directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/file_based/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/common/field_vis_config.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/common/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/common/request.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/actions_panel/actions_panel.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/actions_panel/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/_field_data_card.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/examples_list/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/field_data_card.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/top_values/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_types_select/field_types_select.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/field_types_select/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/fields_panel/fields_panel.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/fields_panel/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/search_panel/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/components/search_panel/search_panel.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/data_loader/data_loader.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/data_loader/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/datavisualizer/index_based/route.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/__mocks__/mock_anomalies_table_data.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/__mocks__/mock_overall_swimlane.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/__snapshots__/explorer_swimlane.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/__tests__/explorer_controller.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/_explorer.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/breadcrumbs.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_influencers_found/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_jobs_found/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_results_found/explorer_no_results_found.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/explorer_no_results_found/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/components/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_anomaly_record.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_chart_data.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_job_config.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_series_config_rare.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__mocks__/mock_series_promises_response.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/_explorer_chart.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/_explorer_chart_tooltip.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/_explorer_charts_container.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/components/explorer_chart_label/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_config_builder.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_config_builder.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_distribution.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_distribution.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_info_tooltip.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_info_tooltip.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_single_metric.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_single_metric.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_charts_container.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_charts_container.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_charts_container.test.mocks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_charts_container_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_charts_container_service.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_charts/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_constants.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_controller.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_dashboard_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_react_wrapper_directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_swimlane.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_swimlane.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_swimlane.test.mocks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/explorer_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/legacy_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/select_limit/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/select_limit/select_limit.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/select_limit/select_limit.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/explorer/select_limit/select_limit_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/abbreviate_whole_number.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/abbreviate_whole_number.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/format_value.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/format_value.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/kibana_field_format.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/metric_change_description.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/metric_change_description.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/number_as_ordinal.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/number_as_ordinal.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/round_to_decimal_place.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/formatters/round_to_decimal_place.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/hacks/toggle_app_link_in_nav.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/_custom_url_editor.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/constants.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/editor.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/editor.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/list.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/list.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/utils.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/components/custom_url_editor/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/_jobs_list.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/create_watch_flyout/email.html (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/create_watch_flyout/email_influencers.html (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/create_watch_flyout/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/create_watch_flyout/watch.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/delete_job_modal/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/edit_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/tabs/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_actions/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_actions/management.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_actions/results.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/_job_details.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/datafeed_preview_tab.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/extract_job_details.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/forecasts_table/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/format_values.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/job_details.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/job_details_pane.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/job_messages_pane.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_details/json_tab.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_filter_bar/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_filter_bar/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_group/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_group/_job_group.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_group/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/job_group/job_group.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list/_jobs_list.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list/job_description.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list/jobs_list.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list_view/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list_view/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_stats_bar/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/ml_job_editor/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/actions_menu.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/new_job_button/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/new_job_button/new_job_button.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/refresh_jobs_list_button/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/start_datafeed_modal/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/start_datafeed_modal/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/components/validate_job.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/jobs_list/jobs.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/chart_loader/chart_loader.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/chart_loader/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/chart_loader/searches.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/components/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/components/job_groups_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/components/time_range_picker.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/index_pattern_context.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/advanced_job_creator.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/configs/combined_job.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/configs/datafeed.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/configs/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/configs/job.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/job_creator.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/job_creator_factory.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/multi_metric_job_creator.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/population_job_creator.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/single_metric_job_creator.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/type_guards.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/util/constants.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/util/default_configs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_creator/util/general.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_runner/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_runner/job_runner.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_validator/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_validator/job_validator.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/job_validator/util.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/results_loader/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/results_loader/results_loader.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/common/results_loader/searches.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/anomaly_chart/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/common/axes.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/common/settings.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/common/utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/event_rate_chart/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/loading_wrapper/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/common/json_editor_flyout/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/common/model_memory_limit/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/common/model_memory_limit/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/hooks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/query/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/datafeed.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/datafeed_step/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_creator_context.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/groups/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/job_details_step/job_details.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/step_types.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/common.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/job_details/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/summary_step/summary.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/time_range_step/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/time_range_step/time_range.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/validation_step/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/validation_step/validation.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/wizard_nav/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/index_or_search/__test__/directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/index_or_search/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/index_or_search/page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/index_or_search/route.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/job_type/__test__/directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/job_type/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/job_type/page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/job_type/route.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/new_job/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/new_job/page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/new_job/route.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/new_job/wizard.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/pages/new_job/wizard_steps.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/__test__/directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/components/create_result_callout.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/components/edit_job.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/components/job_item.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/components/job_settings_form.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/components/kibana_objects.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/components/module_jobs.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/resolvers.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/recognize/route.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/jobs/new_job/utils/new_job_utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/license/__tests__/check_license.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/license/check_license.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/access_denied_page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/jobs_list_page/_analytics_table.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/jobs_list_page/_buttons.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/jobs_list_page/_expanded_row.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/jobs_list_page/_stats_bar.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/jobs_list_page/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/jobs_list/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/management/management_urls.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/ml.svg (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/ml_nodes_check/check_ml_nodes.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/ml_nodes_check/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/analytics_panel/analytics_panel.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/analytics_panel/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/analytics_panel/table.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/anomaly_detection_panel/actions.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/anomaly_detection_panel/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/anomaly_detection_panel/table.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/anomaly_detection_panel/utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/content.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/components/sidebar.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/overview_page.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/overview/route.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/privilege/check_privilege.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/privilege/get_privileges.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/__mocks__/cloudwatch_job_caps_response.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/__mocks__/ml_info_response.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/annotations_service.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/annotations_service.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/calendar_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/field_format_service.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/forecast_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/http_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/job_messages_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/job_service.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/job_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/mapping_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_api_service/annotations.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_api_service/data_frame_analytics.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_api_service/datavisualizer.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_api_service/filters.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_api_service/index.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_api_service/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_api_service/jobs.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_api_service/results.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_server_info.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/ml_server_info.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/new_job_capabilities._service.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/new_job_capabilities_service.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/results_service.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/results_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/table_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/timefilter_refresh_service.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/services/upgrade_service.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/_settings.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/_edit.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/calendar_form/calendar_form.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/calendar_form/calendar_form.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/calendar_form/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/events_table/events_table.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/events_table/events_table.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/events_table/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/import_modal/import_modal.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/import_modal/import_modal.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/import_modal/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/import_modal/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/imported_events/imported_events.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/imported_events/imported_events.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/imported_events/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/new_calendar.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/new_calendar.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/new_calendar.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/new_event_modal/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/new_event_modal/new_event_modal.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/new_event_modal/new_event_modal.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/edit/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/__snapshots__/calendars_list.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/__snapshots__/header.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/_list.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/calendars_list.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/calendars_list.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/calendars_list.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/delete_calendars.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/header.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/header.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/table/__snapshots__/table.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/table/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/table/table.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/calendars/list/table/table.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/_filter_lists.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/add_item_popover/add_item_popover.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/add_item_popover/add_item_popover.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/add_item_popover/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/delete_filter_list_modal/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/edit_description_popover/edit_description_popover.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/edit_description_popover/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/components/filter_list_usage_popover/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/__snapshots__/header.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/_edit.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/edit_filter_list.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/edit_filter_list.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/edit_filter_list.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/header.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/header.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/toolbar.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/toolbar.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/edit/utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/__snapshots__/header.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/__snapshots__/table.test.js.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/filter_lists.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/filter_lists.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/filter_lists.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/header.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/header.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/table.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/filter_lists/list/table.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/index.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/settings.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/settings.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/settings/settings_directive.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/_index.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/_timeseriesexplorer.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/_timeseriesexplorer_annotations.scss (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/breadcrumbs.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/context_chart_mask/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/entity_control/entity_control.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/entity_control/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/forecasting_modal/forecast_progress.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/forecasting_modal/forecasts_list.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/forecasting_modal/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/forecasting_modal/modal.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/forecasting_modal/progress_icon.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/forecasting_modal/progress_states.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/forecasting_modal/run_controls.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/index.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/timeseries_search_service.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/timeseriesexplorer.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/timeseriesexplorer_constants.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/timeseriesexplorer_directive.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/timeseriesexplorer_route.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/timeseriesexplorer/timeseriesexplorer_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/__snapshots__/observable_utils.test.tsx.snap (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/__tests__/app_state_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/__tests__/calc_auto_interval.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/__tests__/chart_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/__tests__/ml_time_buckets.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/__tests__/string_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/app_state_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/calc_auto_interval.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/chart_config_builder.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/chart_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/chart_utils.test.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/custom_url_utils.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/custom_url_utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/date_utils.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/date_utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/field_types_utils.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/field_types_utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/index_utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/inherits.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/ml_error.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/object_utils.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/object_utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/observable_utils.test.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/observable_utils.tsx (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/recently_accessed.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/string_utils.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/string_utils.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/test_utils.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/time_buckets.d.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/time_buckets.js (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/url_utils.test.ts (100%) rename x-pack/legacy/plugins/ml/public/{ => application}/util/url_utils.ts (100%) diff --git a/x-pack/legacy/plugins/ml/public/_app.scss b/x-pack/legacy/plugins/ml/public/application/_app.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/_app.scss rename to x-pack/legacy/plugins/ml/public/application/_app.scss diff --git a/x-pack/legacy/plugins/ml/public/_hacks.scss b/x-pack/legacy/plugins/ml/public/application/_hacks.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/_hacks.scss rename to x-pack/legacy/plugins/ml/public/application/_hacks.scss diff --git a/x-pack/legacy/plugins/ml/public/_variables.scss b/x-pack/legacy/plugins/ml/public/application/_variables.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/_variables.scss rename to x-pack/legacy/plugins/ml/public/application/_variables.scss diff --git a/x-pack/legacy/plugins/ml/public/access_denied/index.tsx b/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/access_denied/index.tsx rename to x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/access_denied/page.tsx b/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/access_denied/page.tsx rename to x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/app.js b/x-pack/legacy/plugins/ml/public/application/app.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/app.js rename to x-pack/legacy/plugins/ml/public/application/app.js diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_description_list/index.tsx rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotation_flyout/index.tsx rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__mocks__/mock_annotations.json b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__mocks__/mock_annotations.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__mocks__/mock_annotations.json rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__mocks__/mock_annotations.json diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.js rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.js rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/annotations_table.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/index.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/annotations_table/index.js rename to x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/annotations/delete_annotation_modal/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/annotations/delete_annotation_modal/index.tsx rename to x-pack/legacy/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/_anomalies_table.scss b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/_anomalies_table.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/_anomalies_table.scss rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/_anomalies_table.scss diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table.test.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_columns.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_constants.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_constants.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomalies_table_constants.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_constants.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.test.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/anomaly_details.test.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/description_cell.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/description_cell.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/description_cell.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/description_cell.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/detector_cell.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/detector_cell.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/detector_cell.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/detector_cell.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/index.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/index.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/influencers_cell.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/influencers_cell.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/influencers_cell.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/influencers_cell.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/links_menu.js rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/index.ts b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.tsx b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/anomalies_table/severity_cell/severity_cell.tsx rename to x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/_chart_tooltip.scss b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/_chart_tooltip.scss rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip.tsx b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip.tsx rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.d.ts b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.d.ts rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.js b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.js rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.test.ts b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.test.ts rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts diff --git a/x-pack/legacy/plugins/ml/public/components/chart_tooltip/index.ts b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/chart_tooltip/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/controls/_controls.scss b/x-pack/legacy/plugins/ml/public/application/components/controls/_controls.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/_controls.scss rename to x-pack/legacy/plugins/ml/public/application/components/controls/_controls.scss diff --git a/x-pack/legacy/plugins/ml/public/components/controls/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/controls/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/controls/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.js b/x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/checkbox_showcharts.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/index.js b/x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/checkbox_showcharts/index.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/checkbox_showcharts/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/index.js b/x-pack/legacy/plugins/ml/public/application/components/controls/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/index.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/index.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_interval/index.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/select_interval.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/select_interval.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval.test.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/select_interval.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_interval/select_interval.test.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_interval/select_interval.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/_select_severity.scss b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/_select_severity.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/_select_severity.scss rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/_select_severity.scss diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/index.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/index.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js diff --git a/x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.test.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/controls/select_severity/select_severity.test.js rename to x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx b/x-pack/legacy/plugins/ml/public/application/components/create_job_link_card/create_job_link_card.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx rename to x-pack/legacy/plugins/ml/public/application/components/create_job_link_card/create_job_link_card.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts b/x-pack/legacy/plugins/ml/public/application/components/create_job_link_card/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/create_job_link_card/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/create_job_link_card/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts b/x-pack/legacy/plugins/ml/public/application/components/custom_hooks/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/custom_hooks/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/custom_hooks/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts b/x-pack/legacy/plugins/ml/public/application/components/custom_hooks/use_partial_state.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/custom_hooks/use_partial_state.ts rename to x-pack/legacy/plugins/ml/public/application/components/custom_hooks/use_partial_state.ts diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts rename to x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js rename to x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/index.ts b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/data_recognizer/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/data_recognizer/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js rename to x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx b/x-pack/legacy/plugins/ml/public/application/components/display_value/display_value.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/display_value/display_value.tsx rename to x-pack/legacy/plugins/ml/public/application/components/display_value/display_value.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/display_value/index.ts b/x-pack/legacy/plugins/ml/public/application/components/display_value/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/display_value/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/display_value/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.js b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.js rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.js diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.scss b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.scss rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.scss diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.test.js b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/entity_cell.test.js rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/entity_cell.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/entity_cell/index.js b/x-pack/legacy/plugins/ml/public/application/components/entity_cell/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/entity_cell/index.js rename to x-pack/legacy/plugins/ml/public/application/components/entity_cell/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/_field_title_bar.scss b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/_field_title_bar.scss rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_field_title_bar.scss diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar.js b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar.js rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar.test.js rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/index.js b/x-pack/legacy/plugins/ml/public/application/components/field_title_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_title_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/components/field_title_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/_field_type_icon.scss b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/_field_type_icon.scss rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_field_type_icon.scss diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.js b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.js rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.test.js b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon.test.js rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/index.js b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/field_type_icon/index.js rename to x-pack/legacy/plugins/ml/public/application/components/field_type_icon/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector.tsx rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/full_time_range_selector/index.tsx rename to x-pack/legacy/plugins/ml/public/application/components/full_time_range_selector/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/influencers_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/influencers_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/influencers_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/influencers_list/_influencers_list.scss b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/_influencers_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/influencers_list/_influencers_list.scss rename to x-pack/legacy/plugins/ml/public/application/components/influencers_list/_influencers_list.scss diff --git a/x-pack/legacy/plugins/ml/public/components/influencers_list/index.js b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/influencers_list/index.js rename to x-pack/legacy/plugins/ml/public/application/components/influencers_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/influencers_list/influencers_list.js b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/influencers_list/influencers_list.js rename to x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/items_grid/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/_items_grid.scss b/x-pack/legacy/plugins/ml/public/application/components/items_grid/_items_grid.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/_items_grid.scss rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/_items_grid.scss diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/index.js b/x-pack/legacy/plugins/ml/public/application/components/items_grid/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/index.js rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/items_grid.js b/x-pack/legacy/plugins/ml/public/application/components/items_grid/items_grid.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/items_grid.js rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/items_grid.js diff --git a/x-pack/legacy/plugins/ml/public/components/items_grid/items_grid_pagination.js b/x-pack/legacy/plugins/ml/public/application/components/items_grid/items_grid_pagination.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/items_grid/items_grid_pagination.js rename to x-pack/legacy/plugins/ml/public/application/components/items_grid/items_grid_pagination.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts b/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/job_message_icon/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx rename to x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/job_messages/index.ts b/x-pack/legacy/plugins/ml/public/application/components/job_messages/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_messages/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/job_messages/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx rename to x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/job_selector/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/_job_selector.scss b/x-pack/legacy/plugins/ml/public/application/components/job_selector/_job_selector.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/_job_selector.scss rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/_job_selector.scss diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/custom_selection_table/custom_selection_table.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/custom_selection_table/custom_selection_table.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/custom_selection_table/custom_selection_table.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/custom_selection_table/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/custom_selection_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/custom_selection_table/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/custom_selection_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/id_badges.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/id_badges.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/id_badges.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/id_badges.test.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/id_badges/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_select_service_utils.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_select_service_utils.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/job_selector_badge.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_badge/job_selector_badge.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/job_selector_table/job_selector_table.test.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/index.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/timerange_bar.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/timerange_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/timerange_bar.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/timerange_bar.js diff --git a/x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/timerange_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/timerange_bar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/job_selector/timerange_bar/timerange_bar.test.js rename to x-pack/legacy/plugins/ml/public/application/components/job_selector/timerange_bar/timerange_bar.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/__snapshots__/kql_filter_bar.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/__tests__/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/__tests__/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/__tests__/utils.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/__tests__/utils.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/__snapshots__/click_outside.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/click_outside.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/click_outside.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/click_outside.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/click_outside.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/click_outside.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/click_outside/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/click_outside/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/__snapshots__/filter_bar.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/filter_bar.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/filter_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/__snapshots__/suggestion.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/suggestion.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/suggestion.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/suggestion.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestion/suggestion.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestion/suggestion.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/__snapshots__/suggestions.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/index.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/index.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.test.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/suggestions/suggestions.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/kql_filter_bar/utils.js rename to x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/__tests__/loading_indicator_directive.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/__tests__/loading_indicator_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/__tests__/loading_indicator_directive.js rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/__tests__/loading_indicator_directive.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/_loading_indicator.scss b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_loading_indicator.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/_loading_indicator.scss rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/_loading_indicator.scss diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/index.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/index.js rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator.html b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.html similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator.html rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.html diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator.js rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator_directive.js b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator_directive.js rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_directive.js diff --git a/x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator_wrapper.html b/x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_wrapper.html similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/loading_indicator/loading_indicator_wrapper.html rename to x-pack/legacy/plugins/ml/public/application/components/loading_indicator/loading_indicator_wrapper.html diff --git a/x-pack/legacy/plugins/ml/public/components/message_call_out/index.js b/x-pack/legacy/plugins/ml/public/application/components/message_call_out/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/message_call_out/index.js rename to x-pack/legacy/plugins/ml/public/application/components/message_call_out/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/message_call_out/message_call_out.js b/x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/message_call_out/message_call_out.js rename to x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/index.ts b/x-pack/legacy/plugins/ml/public/application/components/messagebar/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/messagebar/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/messagebar/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.d.ts b/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.d.ts rename to x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.d.ts diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.js b/x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.js rename to x-pack/legacy/plugins/ml/public/application/components/messagebar/messagebar_service.js diff --git a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/index.ts b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/ml_in_memory_table.tsx b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/ml_in_memory_table.tsx rename to x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts rename to x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/_navigation_menu.scss b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/_navigation_menu.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/_navigation_menu.scss rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/_navigation_menu.scss diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/index.ts b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/main_tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/main_tabs.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/navigation_menu.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/navigation_menu.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/tabs.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/tabs.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/tabs.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/__snapshots__/top_nav.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/index.ts b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.test.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.tsx rename to x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/node_available_warning/index.ts b/x-pack/legacy/plugins/ml/public/application/components/node_available_warning/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/node_available_warning/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/node_available_warning/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/node_available_warning/node_available_warning.tsx b/x-pack/legacy/plugins/ml/public/application/components/node_available_warning/node_available_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/node_available_warning/node_available_warning.tsx rename to x-pack/legacy/plugins/ml/public/application/components/node_available_warning/node_available_warning.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/actions_section.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/actions_section.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/actions_section.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/actions_section.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/condition_expression.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/condition_expression.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/conditions_section.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/conditions_section.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/conditions_section.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/scope_expression.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/scope_expression.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/scope_expression.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/scope_section.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/scope_section.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__snapshots__/scope_section.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/scope_section.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/__tests__/utils.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/__tests__/utils.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/_rule_editor.scss b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/_rule_editor.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/_rule_editor.scss rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/_rule_editor.scss diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/actions_section.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/__snapshots__/detector_description_list.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/_detector_description_list.scss b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/_detector_description_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/_detector_description_list.scss rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/_detector_description_list.scss diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/detector_description_list.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/detector_description_list.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/index.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/components/detector_description_list/index.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/components/detector_description_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/condition_expression.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/conditions_section.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/index.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/index.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/rule_editor_flyout.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/scope_expression.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/scope_section.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/add_to_filter_list_link.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/edit_condition_link.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/add_to_filter_list_link.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/add_to_filter_list_link.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/add_to_filter_list_link.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/delete_rule_modal.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/edit_condition_link.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/index.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/index.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/index.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/rule_action_panel.test.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/select_rule_action.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/select_rule_action/select_rule_action.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js diff --git a/x-pack/legacy/plugins/ml/public/components/rule_editor/utils.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/rule_editor/utils.js rename to x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/_index.scss rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/_stat.scss b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/_stat.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/_stat.scss rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/_stat.scss diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/_stats_bar.scss b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/_stats_bar.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/_stats_bar.scss rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/_stats_bar.scss diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/index.ts b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/stat.tsx b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/stat.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/stat.tsx rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/stat.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/stats_bar.tsx b/x-pack/legacy/plugins/ml/public/application/components/stats_bar/stats_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/stats_bar/stats_bar.tsx rename to x-pack/legacy/plugins/ml/public/application/components/stats_bar/stats_bar.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/upgrade/index.ts b/x-pack/legacy/plugins/ml/public/application/components/upgrade/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/upgrade/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/upgrade/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/upgrade/upgrade_warning.tsx b/x-pack/legacy/plugins/ml/public/application/components/upgrade/upgrade_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/upgrade/upgrade_warning.tsx rename to x-pack/legacy/plugins/ml/public/application/components/upgrade/upgrade_warning.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/__snapshots__/validate_job_view.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/__snapshots__/validate_job_view.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/index.ts b/x-pack/legacy/plugins/ml/public/application/components/validate_job/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/index.ts rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/index.ts diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.d.ts b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.d.ts rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.test.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.test.js rename to x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.test.js diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/index_pattern.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_pattern.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/index_patterns.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_patterns.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/index_patterns.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/index_patterns.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/kibana_config.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_config.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/kibana_config.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_config.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/kibana_context_value.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context_value.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/kibana_context_value.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/kibana_context_value.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/__mocks__/saved_search.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/index.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/index.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/kibana_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/use_current_index_pattern.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/use_current_index_pattern.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_index_pattern.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/use_current_saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/use_current_saved_search.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_current_saved_search.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/kibana/use_kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/kibana/use_kibana_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/kibana/use_kibana_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/mocks.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/mocks.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/use_ui_chrome_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_chrome_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/use_ui_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/use_ui_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/__mocks__/use_ui_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/index.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/index.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/index.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/ui_context.tsx b/x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/ui_context.tsx rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/ui_context.tsx diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/use_ui_chrome_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/use_ui_chrome_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_chrome_context.ts diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/use_ui_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/contexts/ui/use_ui_context.ts rename to x-pack/legacy/plugins/ml/public/application/contexts/ui/use_ui_context.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/fields.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/common/fields.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/_exploration.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/common.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/common.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/page.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/route.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/route.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_list_item.json diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/__mocks__/analytics_stats.json diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/_analytics_table.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/progress_bar.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_advanced_editor/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_create_analytics_flyout.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/create_analytics_flyout_wrapper.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout_wrapper/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_create_analytics_form.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/messages.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button/refresh_analytics_list_button.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/actions.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/route.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/route.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/datavisualizer_selector.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/_file_datavisualizer.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/_file_datavisualizer.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/_file_datavisualizer.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/_file_datavisualizer.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/_about_panel.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/_about_panel.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/_about_panel.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/_about_panel.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/about_panel.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/about_panel.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/about_panel.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/welcome_content.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/about_panel/welcome_content.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/about_panel/welcome_content.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/_analysis_summary.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/analysis_summary.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/analysis_summary.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/analysis_summary.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/analysis_summary.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/analysis_summary/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/analysis_summary/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/bottom_bar.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/bottom_bar/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/bottom_bar/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/_edit_flyout.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/edit_flyout.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/edit_flyout.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/edit_flyout.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/edit_flyout.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/option_lists.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/option_lists.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/option_lists.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/option_lists.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/options.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/options.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/options/options.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/options/options.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides.test.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides.test.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides.test.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides_validation.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides_validation.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/edit_flyout/overrides_validation.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/overrides_validation.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/_experimental_badge.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/experimental_badge.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/experimental_badge.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/experimental_badge.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/experimental_badge.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/experimental_badge/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/experimental_badge/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_field_stats_card.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_fields_stats.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_fields_stats.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_fields_stats.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_fields_stats.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/field_stats_card.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/field_stats_card.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/field_stats_card.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/fields_stats.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/fields_stats.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/get_field_names.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/get_field_names.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/get_field_names.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/get_field_names.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/fields_stats/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/_file_contents.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/_file_contents.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/_file_contents.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/_file_contents.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/file_contents.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/file_contents.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/file_contents.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/file_contents.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_contents/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_contents/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_file_datavisualizer_view.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/constants.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_error_callouts.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/file_datavisualizer_view/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_errors/errors.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_errors/errors.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_errors/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_errors/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_progress/import_progress.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/import_progress.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_progress/import_progress.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/import_progress.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_progress/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_progress/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_progress/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/advanced.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/advanced.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/import_settings.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/import_settings.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/import_settings.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/import_settings.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/simple.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_settings/simple.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/_import_sumary.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/_import_sumary.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/_import_sumary.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/_import_sumary.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/import_summary.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/import_summary.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_summary/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/import_view.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/csv_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/csv_importer.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer_factory.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer_factory.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer_factory.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/importer_factory.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/ndjson_importer.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/sst_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/sst_importer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/sst_importer.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/sst_importer.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_links/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_links/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_links/results_links.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_links/results_links.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/_results_view.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/_results_view.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/_results_view.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/_results_view.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/results_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/results_view/results_view.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_view/results_view.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/index.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/index.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/index.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/overrides.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/overrides.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/overrides.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/overrides.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/utils.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/utils/utils.js rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.js diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/file_based/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/field_vis_config.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/field_vis_config.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/request.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/common/request.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/_field_data_card.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/_field_data_card.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_field_data_card.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/_index.scss b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/_index.scss rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/boolean_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/date_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/document_count_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/geo_point_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/ip_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/keyword_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/not_in_docs_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/other_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/text_content.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/examples_list/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/field_data_card.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/field_data_card.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/loading_indicator/loading_indicator.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_data_builder.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart_tooltip_header.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/top_values/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/top_values/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/top_values/top_values.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/field_types_select.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/field_types_select.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_types_select/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/fields_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/fields_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/fields_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/search_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/search_panel/search_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/data_loader.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/data_loader/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/index.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/route.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/datavisualizer/index_based/route.ts rename to x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/__mocks__/mock_anomalies_table_data.json b/x-pack/legacy/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/__mocks__/mock_anomalies_table_data.json rename to x-pack/legacy/plugins/ml/public/application/explorer/__mocks__/mock_anomalies_table_data.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json b/x-pack/legacy/plugins/ml/public/application/explorer/__mocks__/mock_overall_swimlane.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/__mocks__/mock_overall_swimlane.json rename to x-pack/legacy/plugins/ml/public/application/explorer/__mocks__/mock_overall_swimlane.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/__snapshots__/explorer_swimlane.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/__tests__/explorer_controller.js b/x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_controller.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/__tests__/explorer_controller.js rename to x-pack/legacy/plugins/ml/public/application/explorer/__tests__/explorer_controller.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/_explorer.scss b/x-pack/legacy/plugins/ml/public/application/explorer/_explorer.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/_explorer.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/_explorer.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/_index.scss b/x-pack/legacy/plugins/ml/public/application/explorer/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/_index.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/breadcrumbs.js rename to x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_jobs_found/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/explorer_no_results_found.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/explorer_no_results_found.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/explorer_no_results_found/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/components/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/components/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/components/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_anomaly_record.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_anomaly_record.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_chart_data.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_chart_data.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_chart_data_rare.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_job_config.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_job_config.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_rare.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_config_rare.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_rare.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_config_rare.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_promises_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__mocks__/mock_series_promises_response.json diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_info_tooltip.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_chart.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_chart.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_chart_tooltip.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart_tooltip.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_chart_tooltip.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_chart_tooltip.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_charts_container.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_charts_container.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_explorer_charts_container.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_explorer_charts_container.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_index.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/_index.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/__snapshots__/explorer_chart_label_badge.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_explorer_chart_label_badge.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_index.scss b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/_index.scss rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/explorer_chart_label_badge.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/components/explorer_chart_label/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_info_tooltip.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_info_tooltip.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_charts/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_charts/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_constants.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_constants.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_constants.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_controller.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_dashboard_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_dashboard_service.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_dashboard_service.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_react_wrapper_directive.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_react_wrapper_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_react_wrapper_directive.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_react_wrapper_directive.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_swimlane.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/explorer_utils.js rename to x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/legacy_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/legacy_utils.js rename to x-pack/legacy/plugins/ml/public/application/explorer/legacy_utils.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/select_limit/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/select_limit/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/select_limit/index.js rename to x-pack/legacy/plugins/ml/public/application/explorer/select_limit/index.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit.js b/x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit.js rename to x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit.test.js rename to x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit.test.js diff --git a/x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/explorer/select_limit/select_limit_service.js rename to x-pack/legacy/plugins/ml/public/application/explorer/select_limit/select_limit_service.js diff --git a/x-pack/legacy/plugins/ml/public/formatters/abbreviate_whole_number.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/abbreviate_whole_number.test.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.test.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/abbreviate_whole_number.ts b/x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/abbreviate_whole_number.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/abbreviate_whole_number.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/format_value.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/format_value.test.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/format_value.ts b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/format_value.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/kibana_field_format.ts b/x-pack/legacy/plugins/ml/public/application/formatters/kibana_field_format.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/kibana_field_format.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/kibana_field_format.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/metric_change_description.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/metric_change_description.test.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.test.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/metric_change_description.ts b/x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/metric_change_description.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/metric_change_description.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/number_as_ordinal.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/number_as_ordinal.test.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.test.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/number_as_ordinal.ts b/x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/number_as_ordinal.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/number_as_ordinal.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/round_to_decimal_place.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/round_to_decimal_place.test.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts diff --git a/x-pack/legacy/plugins/ml/public/formatters/round_to_decimal_place.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/formatters/round_to_decimal_place.ts rename to x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts diff --git a/x-pack/legacy/plugins/ml/public/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/hacks/toggle_app_link_in_nav.js rename to x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/list.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/_custom_url_editor.scss b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/_custom_url_editor.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/_custom_url_editor.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/_custom_url_editor.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/constants.ts b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/constants.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/constants.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/constants.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.js rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.test.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/editor.test.js rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.test.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.test.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.test.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/list.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.d.ts b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.d.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/components/custom_url_editor/utils.js rename to x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/_jobs_list.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/_jobs_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/_jobs_list.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/_jobs_list.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email.html b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email.html rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email.html diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email_influencers.html b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email_influencers.html similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email_influencers.html rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/email_influencers.html diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/watch.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/watch.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/delete_job_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/_edit_job_flyout.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/management.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/management.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/_job_details.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/_job_details.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/datafeed_preview_tab.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/forecasts_table/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/format_values.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/format_values.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details_pane.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details_pane.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/json_tab.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/json_tab.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/json_tab.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/json_tab.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/_job_filter_bar.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/_job_group.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/_job_group.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/_job_group.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/_job_group.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/_jobs_list.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/_jobs_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/_jobs_list.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/_jobs_list.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/job_description.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/job_description.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.d.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/_multi_job_actions.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/actions_menu.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/_group_selector.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_group_list.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/group_list.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/_new_group_input.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/new_group_input/new_group_input.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/new_job_button/new_job_button.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/refresh_jobs_list_button/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/refresh_jobs_list_button/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/refresh_jobs_list_button/refresh_jobs_list_button.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/_time_range_selector.scss diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/chart_loader.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/searches.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/chart_loader/searches.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/job_groups_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/job_groups_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/time_range_picker.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/time_range_picker.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/components/time_range_picker.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/time_range_picker.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/index_pattern_context.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/index_pattern_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/index_pattern_context.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/index_pattern_context.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/advanced_job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/combined_job.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/combined_job.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/combined_job.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/combined_job.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/datafeed.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/datafeed.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/job.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/configs/job.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator_factory.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/job_creator_factory.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/multi_metric_job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/population_job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/single_metric_job_creator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/type_guards.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/type_guards.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/constants.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/constants.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/constants.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/constants.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/default_configs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/default_configs.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_creator/util/general.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/job_runner.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_runner/job_runner.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/job_validator.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/util.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/job_validator/util.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/results_loader.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/searches.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/common/results_loader/searches.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomaly_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/line.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/model_bounds.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/scatter.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/axes.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/axes.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/axes.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/axes.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/settings.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/settings.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/common/utils.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/utils.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/event_rate_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/event_rate_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/loading_wrapper/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/loading_wrapper/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/loading_wrapper/loading_wrapper.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/model_memory_limit/model_memory_limit_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/hooks.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/hooks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/hooks.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/hooks.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/datafeed.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/datafeed.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/datafeed_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_creator_context.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_creator_context.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/additional_section.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/dedicated_index_switch.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/dedicated_index/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/groups/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_description/job_description_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/job_id/job_id_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/job_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/job_details_step/job_details.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/job_details.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/descriptions.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/index.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/advanced_view.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/extra.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection_summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/bucket_span_input.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/bucket_span_estimator.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/multi_metric_view.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/population_view.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/single_metric_view.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/animate_split_hook.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/step_types.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/step_types.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/step_types.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/step_types.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/common.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/common.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/common.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/common.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_details/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_details/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_details/job_details.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_progress/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/job_progress/job_progress.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/summary_step/summary.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/time_range_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/time_range_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/time_range_step/time_range.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/validation_step/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/validation_step/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/validation_step/validation.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/wizard_nav/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/wizard_nav/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/wizard_nav/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/wizard_nav/index.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/wizard_nav/wizard_nav.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/__test__/directive.js rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/page.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/index_or_search/route.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/__test__/directive.js rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/page.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/job_type/route.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/page.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/route.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/pages/new_job/wizard_steps.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/__test__/directive.js rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/create_result_callout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/create_result_callout.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/edit_job.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/edit_job.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_item.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_item.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_settings_form.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/job_settings_form.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/kibana_objects.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/kibana_objects.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/module_jobs.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/components/module_jobs.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/page.tsx rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/resolvers.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/recognize/route.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/jobs/new_job/utils/new_job_utils.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/license/__tests__/check_license.js b/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/license/__tests__/check_license.js rename to x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js diff --git a/x-pack/legacy/plugins/ml/public/license/check_license.tsx b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/license/check_license.tsx rename to x-pack/legacy/plugins/ml/public/application/license/check_license.tsx diff --git a/x-pack/legacy/plugins/ml/public/management/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/_index.scss rename to x-pack/legacy/plugins/ml/public/application/management/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/management/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/management/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/management/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/management/index.ts b/x-pack/legacy/plugins/ml/public/application/management/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/index.ts rename to x-pack/legacy/plugins/ml/public/application/management/index.ts diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/_index.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/access_denied_page.tsx b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/access_denied_page.tsx rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/index.ts b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/index.ts rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/index.ts diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_analytics_table.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_analytics_table.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_analytics_table.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_analytics_table.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_buttons.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_buttons.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_buttons.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_buttons.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_expanded_row.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_expanded_row.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_stats_bar.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_stats_bar.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_stats_bar.scss rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/_stats_bar.scss diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/index.ts b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/index.ts rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/index.ts diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/jobs_list/index.ts rename to x-pack/legacy/plugins/ml/public/application/management/jobs_list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/management/management_urls.ts b/x-pack/legacy/plugins/ml/public/application/management/management_urls.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/management/management_urls.ts rename to x-pack/legacy/plugins/ml/public/application/management/management_urls.ts diff --git a/x-pack/legacy/plugins/ml/public/ml.svg b/x-pack/legacy/plugins/ml/public/application/ml.svg similarity index 100% rename from x-pack/legacy/plugins/ml/public/ml.svg rename to x-pack/legacy/plugins/ml/public/application/ml.svg diff --git a/x-pack/legacy/plugins/ml/public/ml_nodes_check/check_ml_nodes.ts b/x-pack/legacy/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/ml_nodes_check/check_ml_nodes.ts rename to x-pack/legacy/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts diff --git a/x-pack/legacy/plugins/ml/public/ml_nodes_check/index.ts b/x-pack/legacy/plugins/ml/public/application/ml_nodes_check/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/ml_nodes_check/index.ts rename to x-pack/legacy/plugins/ml/public/application/ml_nodes_check/index.ts diff --git a/x-pack/legacy/plugins/ml/public/overview/_index.scss b/x-pack/legacy/plugins/ml/public/application/overview/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/_index.scss rename to x-pack/legacy/plugins/ml/public/application/overview/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/overview/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/overview/components/_index.scss b/x-pack/legacy/plugins/ml/public/application/overview/components/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/_index.scss rename to x-pack/legacy/plugins/ml/public/application/overview/components/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/actions.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/index.ts rename to x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/index.ts diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/table.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/table.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/utils.ts b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/utils.ts rename to x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts diff --git a/x-pack/legacy/plugins/ml/public/overview/components/content.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/content.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/components/sidebar.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/directive.tsx b/x-pack/legacy/plugins/ml/public/application/overview/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/index.ts b/x-pack/legacy/plugins/ml/public/application/overview/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/index.ts rename to x-pack/legacy/plugins/ml/public/application/overview/index.ts diff --git a/x-pack/legacy/plugins/ml/public/overview/overview_page.tsx b/x-pack/legacy/plugins/ml/public/application/overview/overview_page.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/overview_page.tsx rename to x-pack/legacy/plugins/ml/public/application/overview/overview_page.tsx diff --git a/x-pack/legacy/plugins/ml/public/overview/route.ts b/x-pack/legacy/plugins/ml/public/application/overview/route.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/overview/route.ts rename to x-pack/legacy/plugins/ml/public/application/overview/route.ts diff --git a/x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/privilege/check_privilege.ts rename to x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts diff --git a/x-pack/legacy/plugins/ml/public/privilege/get_privileges.ts b/x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/privilege/get_privileges.ts rename to x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts diff --git a/x-pack/legacy/plugins/ml/public/services/__mocks__/cloudwatch_job_caps_response.json b/x-pack/legacy/plugins/ml/public/application/services/__mocks__/cloudwatch_job_caps_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/__mocks__/cloudwatch_job_caps_response.json rename to x-pack/legacy/plugins/ml/public/application/services/__mocks__/cloudwatch_job_caps_response.json diff --git a/x-pack/legacy/plugins/ml/public/services/__mocks__/ml_info_response.json b/x-pack/legacy/plugins/ml/public/application/services/__mocks__/ml_info_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/__mocks__/ml_info_response.json rename to x-pack/legacy/plugins/ml/public/application/services/__mocks__/ml_info_response.json diff --git a/x-pack/legacy/plugins/ml/public/services/annotations_service.test.tsx b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/annotations_service.test.tsx rename to x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/services/annotations_service.tsx b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/annotations_service.tsx rename to x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx diff --git a/x-pack/legacy/plugins/ml/public/services/calendar_service.js b/x-pack/legacy/plugins/ml/public/application/services/calendar_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/calendar_service.js rename to x-pack/legacy/plugins/ml/public/application/services/calendar_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/field_format_service.ts b/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/field_format_service.ts rename to x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts diff --git a/x-pack/legacy/plugins/ml/public/services/forecast_service.js b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/forecast_service.js rename to x-pack/legacy/plugins/ml/public/application/services/forecast_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/http_service.js b/x-pack/legacy/plugins/ml/public/application/services/http_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/http_service.js rename to x-pack/legacy/plugins/ml/public/application/services/http_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/job_messages_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/job_messages_service.js rename to x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/job_service.d.ts rename to x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/job_service.js rename to x-pack/legacy/plugins/ml/public/application/services/job_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/mapping_service.js b/x-pack/legacy/plugins/ml/public/application/services/mapping_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/mapping_service.js rename to x-pack/legacy/plugins/ml/public/application/services/mapping_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/annotations.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/annotations.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/data_frame_analytics.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/data_frame_analytics.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/datavisualizer.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/datavisualizer.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/filters.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/filters.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/index.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/jobs.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_api_service/results.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js diff --git a/x-pack/legacy/plugins/ml/public/services/ml_server_info.test.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_server_info.test.ts rename to x-pack/legacy/plugins/ml/public/application/services/ml_server_info.test.ts diff --git a/x-pack/legacy/plugins/ml/public/services/ml_server_info.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/ml_server_info.ts rename to x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts diff --git a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities._service.test.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities._service.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/new_job_capabilities._service.test.ts rename to x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities._service.test.ts diff --git a/x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/new_job_capabilities_service.ts rename to x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts diff --git a/x-pack/legacy/plugins/ml/public/services/results_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/results_service.d.ts rename to x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts diff --git a/x-pack/legacy/plugins/ml/public/services/results_service.js b/x-pack/legacy/plugins/ml/public/application/services/results_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/results_service.js rename to x-pack/legacy/plugins/ml/public/application/services/results_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/table_service.js b/x-pack/legacy/plugins/ml/public/application/services/table_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/table_service.js rename to x-pack/legacy/plugins/ml/public/application/services/table_service.js diff --git a/x-pack/legacy/plugins/ml/public/services/timefilter_refresh_service.tsx b/x-pack/legacy/plugins/ml/public/application/services/timefilter_refresh_service.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/timefilter_refresh_service.tsx rename to x-pack/legacy/plugins/ml/public/application/services/timefilter_refresh_service.tsx diff --git a/x-pack/legacy/plugins/ml/public/services/upgrade_service.ts b/x-pack/legacy/plugins/ml/public/application/services/upgrade_service.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/services/upgrade_service.ts rename to x-pack/legacy/plugins/ml/public/application/services/upgrade_service.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/_settings.scss b/x-pack/legacy/plugins/ml/public/application/settings/_settings.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/_settings.scss rename to x-pack/legacy/plugins/ml/public/application/settings/_settings.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/_edit.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/_edit.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/_edit.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/_edit.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/calendar_form.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/calendar_form/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/events_table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/events_table.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/events_table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/events_table.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/events_table/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/__snapshots__/import_modal.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/import_modal.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/import_modal/utils.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/import_modal/utils.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/__snapshots__/imported_events.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/imported_events.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/imported_events/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.d.ts rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_event_modal/new_event_modal.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/edit/utils.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/__snapshots__/calendars_list.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/__snapshots__/calendars_list.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/__snapshots__/header.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/header.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/__snapshots__/header.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/__snapshots__/header.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/_list.scss b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/_list.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/_list.scss rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/_list.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.d.ts rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.d.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/delete_calendars.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/delete_calendars.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/delete_calendars.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/header.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/header.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/header.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/header.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/table/__snapshots__/table.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/table/index.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/table/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/table/table.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/table/table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/calendars/list/table/table.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/_filter_lists.scss b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/_filter_lists.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/_filter_lists.scss rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/_filter_lists.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/add_item_popover.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/add_item_popover.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/add_item_popover.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/add_item_popover.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/index.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/add_item_popover/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_lists.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/index.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/delete_filter_list_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/edit_description_popover.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/edit_description_popover.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/index.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/edit_description_popover/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/__snapshots__/filter_list_usage_popover.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/filter_list_usage_popover.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/index.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/components/filter_list_usage_popover/index.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/components/filter_list_usage_popover/index.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/header.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/header.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/header.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/toolbar.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/_edit.scss b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/_edit.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/_edit.scss rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/_edit.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/_index.scss b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/_index.scss rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.d.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/header.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/header.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/header.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/header.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/toolbar.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/toolbar.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/toolbar.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/toolbar.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/toolbar.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/toolbar.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/toolbar.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/toolbar.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/utils.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/filter_lists.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/header.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/header.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/header.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/__snapshots__/table.test.js.snap rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.d.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.d.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/header.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/header.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/header.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/header.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/header.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/table.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/table.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/table.test.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/filter_lists/list/table.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/index.ts rename to x-pack/legacy/plugins/ml/public/application/settings/index.ts diff --git a/x-pack/legacy/plugins/ml/public/settings/settings.test.js b/x-pack/legacy/plugins/ml/public/application/settings/settings.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/settings.test.js rename to x-pack/legacy/plugins/ml/public/application/settings/settings.test.js diff --git a/x-pack/legacy/plugins/ml/public/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/settings.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/settings.tsx diff --git a/x-pack/legacy/plugins/ml/public/settings/settings_directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/settings_directive.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/settings/settings_directive.tsx rename to x-pack/legacy/plugins/ml/public/application/settings/settings_directive.tsx diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/__tests__/timeseriesexplorer_directive.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_index.scss b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_index.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/_index.scss rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_index.scss diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer.scss rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer.scss diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer_annotations.scss b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/_timeseriesexplorer_annotations.scss rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/breadcrumbs.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/context_chart_mask/context_chart_mask.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/context_chart_mask/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/context_chart_mask/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/context_chart_mask/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/context_chart_mask/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/entity_control.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/entity_control.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/entity_control/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecast_progress.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecast_progress.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecast_progress.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecast_progress.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasts_list.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasts_list.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/modal.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/modal.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/progress_icon.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/progress_icon.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/progress_icon.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/progress_icon.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/progress_states.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/progress_states.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/progress_states.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/progress_states.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/run_controls.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/__mocks__/mock_annotations_overlap.json diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.test.mocks.ts diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.test.ts diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_chart_data/timeseriesexplorer_no_chart_data.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_constants.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_constants.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_directive.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_directive.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_route.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_route.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer_utils.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/__snapshots__/observable_utils.test.tsx.snap b/x-pack/legacy/plugins/ml/public/application/util/__snapshots__/observable_utils.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__snapshots__/observable_utils.test.tsx.snap rename to x-pack/legacy/plugins/ml/public/application/util/__snapshots__/observable_utils.test.tsx.snap diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/app_state_utils.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/app_state_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/app_state_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/app_state_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/calc_auto_interval.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/calc_auto_interval.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/calc_auto_interval.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/calc_auto_interval.js diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/chart_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/ml_time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/ml_time_buckets.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/ml_time_buckets.js diff --git a/x-pack/legacy/plugins/ml/public/util/__tests__/string_utils.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/string_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/__tests__/string_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/__tests__/string_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/app_state_utils.js b/x-pack/legacy/plugins/ml/public/application/util/app_state_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/app_state_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/app_state_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/calc_auto_interval.js b/x-pack/legacy/plugins/ml/public/application/util/calc_auto_interval.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/calc_auto_interval.js rename to x-pack/legacy/plugins/ml/public/application/util/calc_auto_interval.js diff --git a/x-pack/legacy/plugins/ml/public/util/chart_config_builder.js b/x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/chart_config_builder.js rename to x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js diff --git a/x-pack/legacy/plugins/ml/public/util/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/chart_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/chart_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/chart_utils.test.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/chart_utils.test.js rename to x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js diff --git a/x-pack/legacy/plugins/ml/public/util/custom_url_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/custom_url_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts diff --git a/x-pack/legacy/plugins/ml/public/util/custom_url_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/custom_url_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/date_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/date_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/date_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/date_utils.test.ts diff --git a/x-pack/legacy/plugins/ml/public/util/date_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/date_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/date_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/date_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/field_types_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/field_types_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts diff --git a/x-pack/legacy/plugins/ml/public/util/field_types_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/field_types_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/index_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/index_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/inherits.js b/x-pack/legacy/plugins/ml/public/application/util/inherits.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/inherits.js rename to x-pack/legacy/plugins/ml/public/application/util/inherits.js diff --git a/x-pack/legacy/plugins/ml/public/util/ml_error.js b/x-pack/legacy/plugins/ml/public/application/util/ml_error.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/ml_error.js rename to x-pack/legacy/plugins/ml/public/application/util/ml_error.js diff --git a/x-pack/legacy/plugins/ml/public/util/object_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/object_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/object_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/object_utils.test.ts diff --git a/x-pack/legacy/plugins/ml/public/util/object_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/object_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/object_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/object_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/observable_utils.test.tsx b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.test.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/observable_utils.test.tsx rename to x-pack/legacy/plugins/ml/public/application/util/observable_utils.test.tsx diff --git a/x-pack/legacy/plugins/ml/public/util/observable_utils.tsx b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/observable_utils.tsx rename to x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx diff --git a/x-pack/legacy/plugins/ml/public/util/recently_accessed.ts b/x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/recently_accessed.ts rename to x-pack/legacy/plugins/ml/public/application/util/recently_accessed.ts diff --git a/x-pack/legacy/plugins/ml/public/util/string_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/util/string_utils.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/string_utils.d.ts rename to x-pack/legacy/plugins/ml/public/application/util/string_utils.d.ts diff --git a/x-pack/legacy/plugins/ml/public/util/string_utils.js b/x-pack/legacy/plugins/ml/public/application/util/string_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/string_utils.js rename to x-pack/legacy/plugins/ml/public/application/util/string_utils.js diff --git a/x-pack/legacy/plugins/ml/public/util/test_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/test_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/test_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/test_utils.ts diff --git a/x-pack/legacy/plugins/ml/public/util/time_buckets.d.ts b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/time_buckets.d.ts rename to x-pack/legacy/plugins/ml/public/application/util/time_buckets.d.ts diff --git a/x-pack/legacy/plugins/ml/public/util/time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/time_buckets.js rename to x-pack/legacy/plugins/ml/public/application/util/time_buckets.js diff --git a/x-pack/legacy/plugins/ml/public/util/url_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/url_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/url_utils.test.ts rename to x-pack/legacy/plugins/ml/public/application/util/url_utils.test.ts diff --git a/x-pack/legacy/plugins/ml/public/util/url_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/url_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/util/url_utils.ts rename to x-pack/legacy/plugins/ml/public/application/util/url_utils.ts From 9191d334c0f3b3812c94a9d62548a909f73270d6 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 22 Nov 2019 14:57:08 +0000 Subject: [PATCH 039/128] updating paths --- src/dev/precommit_hook/casing_check_config.js | 1 - x-pack/README.md | 2 +- .../legacy/plugins/ml/common/types/modules.ts | 2 +- .../plugins/ml/common/util/job_utils.js | 2 +- x-pack/legacy/plugins/ml/index.ts | 8 +-- .../plugins/ml/public/application/app.js | 22 ++++---- .../annotation_description_list/index.tsx | 2 +- .../annotation_flyout/index.test.tsx | 2 +- .../annotations/annotation_flyout/index.tsx | 2 +- .../annotations_table/annotations_table.js | 4 +- .../annotations_table.test.js | 2 +- .../annotations_table.test.mocks.ts | 2 +- .../anomalies_table/anomalies_table.js | 2 +- .../anomalies_table_columns.js | 2 +- .../anomalies_table/anomaly_details.js | 4 +- .../components/anomalies_table/links_menu.js | 8 +-- .../severity_cell/severity_cell.tsx | 4 +- .../chart_tooltip/chart_tooltip.tsx | 2 +- .../select_severity/select_severity.js | 2 +- .../data_recognizer/data_recognizer.js | 2 +- .../field_type_icon/field_type_icon.js | 2 +- .../field_type_icon/field_type_icon.test.js | 2 +- .../influencers_list/influencers_list.js | 4 +- .../job_message_icon/job_message_icon.tsx | 2 +- .../components/job_messages/job_messages.tsx | 2 +- .../components/job_selector/job_selector.js | 2 +- .../job_selector_badge/job_selector_badge.js | 2 +- .../job_selector_table/job_selector_table.js | 2 +- .../message_call_out/message_call_out.js | 2 +- .../components/rule_editor/__tests__/utils.js | 2 +- .../components/rule_editor/actions_section.js | 2 +- .../rule_editor/actions_section.test.js | 2 +- .../rule_editor/condition_expression.js | 2 +- .../rule_editor/condition_expression.test.js | 2 +- .../rule_editor/conditions_section.test.js | 2 +- .../rule_editor/rule_editor_flyout.js | 4 +- .../rule_editor/scope_expression.js | 2 +- .../rule_editor/scope_expression.test.js | 2 +- .../rule_editor/scope_section.test.js | 2 +- .../select_rule_action/edit_condition_link.js | 2 +- .../edit_condition_link.test.js | 2 +- .../rule_action_panel.test.js | 2 +- .../components/rule_editor/utils.js | 4 +- .../validate_job/validate_job_view.js | 4 +- .../contexts/kibana/__mocks__/saved_search.ts | 2 +- .../data_frame_analytics/breadcrumbs.ts | 2 +- .../data_frame_analytics/common/analytics.ts | 2 +- .../pages/analytics_exploration/directive.tsx | 2 +- .../expanded_row_messages_pane.tsx | 2 +- .../analytics_list/use_refresh_interval.ts | 2 +- .../create_analytics_button.test.tsx | 2 +- .../create_analytics_flyout.test.tsx | 2 +- .../create_analytics_form.test.tsx | 2 +- .../create_analytics_form.tsx | 4 +- .../pages/analytics_management/directive.tsx | 2 +- .../use_create_analytics_form/reducer.ts | 8 +-- .../hooks/use_create_analytics_form/state.ts | 2 +- .../application/datavisualizer/breadcrumbs.ts | 2 +- .../datavisualizer/file_based/breadcrumbs.ts | 2 +- .../components/fields_stats/fields_stats.js | 2 +- .../file_datavisualizer_view.js | 2 +- .../import_view/importer/csv_importer.js | 2 +- .../file_based/file_datavisualizer.tsx | 2 +- .../file_datavisualizer_directive.tsx | 2 +- .../datavisualizer/index_based/breadcrumbs.ts | 2 +- .../index_based/common/field_vis_config.ts | 2 +- .../index_based/common/request.ts | 2 +- .../field_data_card/field_data_card.tsx | 4 +- .../field_types_select/field_types_select.tsx | 2 +- .../components/fields_panel/fields_panel.tsx | 2 +- .../components/search_panel/search_panel.tsx | 4 +- .../index_based/data_loader/data_loader.ts | 2 +- .../datavisualizer/index_based/directive.tsx | 2 +- .../datavisualizer/index_based/page.tsx | 6 +-- .../application/explorer/breadcrumbs.js | 2 +- .../public/application/explorer/explorer.js | 4 +- .../explorer_chart_config_builder.js | 4 +- .../explorer_chart_distribution.js | 2 +- .../explorer_chart_single_metric.js | 2 +- .../explorer_charts_container_service.js | 4 +- .../explorer/explorer_charts/index.js | 2 +- .../explorer/explorer_controller.js | 4 +- .../application/explorer/explorer_swimlane.js | 2 +- .../application/explorer/explorer_utils.js | 10 ++-- .../ml/public/application/explorer/index.js | 12 ++--- .../formatters/format_value.test.ts | 2 +- .../application/formatters/format_value.ts | 2 +- .../hacks/toggle_app_link_in_nav.js | 4 +- .../ml/public/application/jobs/breadcrumbs.ts | 2 +- .../components/custom_url_editor/editor.js | 2 +- .../components/custom_url_editor/list.tsx | 4 +- .../components/custom_url_editor/utils.d.ts | 2 +- .../components/custom_url_editor/utils.js | 6 +-- .../create_watch_flyout/create_watch_view.js | 2 +- .../components/create_watch_flyout/watch.js | 2 +- .../delete_job_modal/delete_job_modal.js | 2 +- .../components/edit_job_flyout/edit_utils.js | 4 +- .../edit_job_flyout/tabs/datafeed.js | 4 +- .../edit_job_flyout/tabs/detectors.js | 4 +- .../edit_job_flyout/tabs/job_details.js | 2 +- .../job_details/datafeed_preview_tab.js | 6 +-- .../job_details/extract_job_details.js | 2 +- .../forecasts_table/forecasts_table.js | 8 +-- .../components/job_details/format_values.js | 2 +- .../job_details/job_messages_pane.tsx | 2 +- .../job_filter_bar/job_filter_bar.js | 2 +- .../components/job_group/job_group.js | 2 +- .../jobs_list_view/jobs_list_view.js | 4 +- .../jobs_stats_bar/jobs_stats_bar.js | 2 +- .../multi_job_actions/actions_menu.js | 4 +- .../group_selector/group_selector.js | 2 +- .../new_job_button/new_job_button.js | 4 +- .../jobs/jobs_list/components/utils.js | 10 ++-- .../jobs/jobs_list/components/validate_job.js | 2 +- .../application/jobs/jobs_list/directive.js | 12 ++--- .../common/chart_loader/chart_loader.ts | 4 +- .../common/components/job_groups_input.tsx | 2 +- .../job_creator/advanced_job_creator.ts | 4 +- .../common/job_creator/configs/datafeed.ts | 2 +- .../new_job/common/job_creator/job_creator.ts | 10 ++-- .../job_creator/multi_metric_job_creator.ts | 7 ++- .../job_creator/population_job_creator.ts | 7 ++- .../job_creator/single_metric_job_creator.ts | 6 +-- .../job_creator/util/default_configs.ts | 4 +- .../common/job_creator/util/general.ts | 8 +-- .../new_job/common/job_runner/job_runner.ts | 2 +- .../common/job_validator/job_validator.ts | 5 +- .../jobs/new_job/common/job_validator/util.ts | 7 ++- .../common/results_loader/results_loader.ts | 8 +-- .../new_job/common/results_loader/searches.ts | 2 +- .../charts/anomaly_chart/anomalies.tsx | 4 +- .../datafeed_preview_flyout.tsx | 2 +- .../json_editor_flyout/json_editor_flyout.tsx | 2 +- .../components/frequency/frequency_input.tsx | 2 +- .../components/query/query_input.tsx | 2 +- .../time_field/time_field_select.tsx | 2 +- .../pages/components/job_creator_context.ts | 2 +- .../components/groups/groups_input.tsx | 2 +- .../advanced_detector_modal.tsx | 2 +- .../advanced_view/metric_selection.tsx | 2 +- .../advanced_view/metric_selector.tsx | 2 +- .../components/agg_select/agg_select.tsx | 2 +- .../estimate_bucket_span.ts | 2 +- .../categorization_field_select.tsx | 2 +- .../detector_title/detector_title.tsx | 2 +- .../influencers/influencers_select.tsx | 2 +- .../multi_metric_view/chart_grid.tsx | 2 +- .../multi_metric_view/metric_selection.tsx | 2 +- .../multi_metric_view/metric_selector.tsx | 2 +- .../components/population_view/chart_grid.tsx | 2 +- .../population_view/metric_selection.tsx | 2 +- .../metric_selection_summary.tsx | 2 +- .../population_view/metric_selector.tsx | 2 +- .../single_metric_view/metric_selection.tsx | 2 +- .../sparse_data/sparse_data_switch.tsx | 2 +- .../components/split_cards/split_cards.tsx | 2 +- .../components/split_field/by_field.tsx | 2 +- .../split_field/split_field_select.tsx | 2 +- .../summary_count_field_select.tsx | 2 +- .../datafeed_details/datafeed_details.tsx | 2 +- .../post_save_options/post_save_options.tsx | 6 +-- .../index_or_search/__test__/directive.js | 2 +- .../pages/index_or_search/directive.tsx | 2 +- .../pages/job_type/__test__/directive.js | 2 +- .../jobs/new_job/pages/job_type/directive.tsx | 4 +- .../jobs/new_job/pages/new_job/directive.tsx | 2 +- .../jobs/new_job/pages/new_job/route.ts | 2 +- .../new_job/recognize/__test__/directive.js | 2 +- .../new_job/recognize/components/edit_job.tsx | 8 +-- .../new_job/recognize/components/job_item.tsx | 4 +- .../components/job_settings_form.tsx | 4 +- .../recognize/components/module_jobs.tsx | 2 +- .../jobs/new_job/recognize/directive.tsx | 4 +- .../jobs/new_job/recognize/page.tsx | 2 +- .../jobs/new_job/utils/new_job_utils.ts | 2 +- .../license/__tests__/check_license.js | 4 +- .../application/license/check_license.tsx | 6 +-- .../public/application/management/_index.scss | 2 +- .../ml/public/application/management/index.ts | 8 +-- .../management/jobs_list/_index.scss | 2 +- .../jobs_list/components/_index.scss | 8 +-- .../jobs_list_page/jobs_list_page.tsx | 2 +- .../public/application/overview/_index.scss | 2 +- .../application/overview/breadcrumbs.ts | 2 +- .../anomaly_detection_panel/actions.tsx | 2 +- .../anomaly_detection_panel.tsx | 4 +- .../anomaly_detection_panel/table.tsx | 6 +-- .../anomaly_detection_panel/utils.ts | 4 +- .../overview/components/content.tsx | 2 +- .../application/privilege/check_privilege.ts | 2 +- .../application/privilege/get_privileges.ts | 2 +- .../services/annotations_service.test.tsx | 2 +- .../services/annotations_service.tsx | 4 +- .../application/services/calendar_service.js | 6 +-- .../services/field_format_service.ts | 4 +- .../application/services/forecast_service.js | 2 +- .../services/job_messages_service.js | 4 +- .../application/services/job_service.js | 4 +- .../application/services/mapping_service.js | 2 +- .../services/ml_api_service/annotations.js | 2 +- .../ml_api_service/data_frame_analytics.js | 2 +- .../services/ml_api_service/datavisualizer.js | 2 +- .../services/ml_api_service/filters.js | 2 +- .../services/ml_api_service/index.d.ts | 16 +++--- .../services/ml_api_service/index.js | 2 +- .../services/ml_api_service/jobs.js | 2 +- .../services/ml_api_service/results.js | 2 +- .../services/new_job_capabilities_service.ts | 4 +- .../application/services/results_service.js | 6 +-- .../application/settings/breadcrumbs.ts | 2 +- .../edit/calendar_form/calendar_form.js | 2 +- .../edit/imported_events/imported_events.js | 2 +- .../settings/calendars/edit/new_calendar.js | 4 +- .../edit/new_event_modal/new_event_modal.js | 2 +- .../settings/calendars/edit/utils.js | 2 +- .../settings/calendars/list/calendars_list.js | 2 +- .../settings/filter_lists/edit/utils.js | 4 +- .../timeseriesexplorer/breadcrumbs.js | 2 +- .../forecasting_modal/forecasting_modal.js | 14 +++--- .../components/forecasting_modal/modal.js | 2 +- .../forecasting_modal/run_controls.js | 6 +-- .../timeseries_chart/timeseries_chart.d.ts | 4 +- .../timeseries_chart/timeseries_chart.js | 2 +- .../timeseries_chart_annotations.ts | 6 +-- .../timeseries_search_service.js | 2 +- .../timeseriesexplorer/timeseriesexplorer.js | 8 +-- .../timeseriesexplorer_utils.js | 6 +-- .../application/util/__tests__/chart_utils.js | 2 +- .../application/util/chart_config_builder.js | 2 +- .../ml/public/application/util/chart_utils.js | 4 +- .../application/util/custom_url_utils.test.ts | 4 +- .../application/util/custom_url_utils.ts | 6 +-- .../util/field_types_utils.test.ts | 4 +- .../application/util/field_types_utils.ts | 4 +- .../ml/public/application/util/index_utils.ts | 4 +- .../ml/public/application/util/ml_error.js | 2 +- .../application/util/observable_utils.tsx | 2 +- .../public/application/util/time_buckets.js | 4 +- x-pack/legacy/plugins/ml/public/index.scss | 50 +++++++++---------- .../transform/public/shared_imports.ts | 4 +- 240 files changed, 428 insertions(+), 415 deletions(-) diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index edd818e1b42de..e18852e353b00 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -167,7 +167,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/legacy/plugins/index_management/public/lib/editSettings.js', 'x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js', 'x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', - 'x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/email-influencers.html', 'x-pack/legacy/plugins/monitoring/public/icons/alert-blue.svg', 'x-pack/legacy/plugins/monitoring/public/icons/health-gray.svg', 'x-pack/legacy/plugins/monitoring/public/icons/health-green.svg', diff --git a/x-pack/README.md b/x-pack/README.md index bd50181afee69..3f1fc819d145b 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -23,7 +23,7 @@ By default, this will also set the password for native realm accounts to the pas Examples: - Run the jest test case whose description matches 'filtering should skip values of null': - `cd x-pack && yarn test:jest -t 'filtering should skip values of null' plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js` + `cd x-pack && yarn test:jest -t 'filtering should skip values of null' plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js` - Run the x-pack api integration test case whose description matches the given string: `node scripts/functional_tests_server --config x-pack/test/api_integration/config.js` `node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'` diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts index 8fdc0d13a78a9..52259d8748a95 100644 --- a/x-pack/legacy/plugins/ml/common/types/modules.ts +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectAttributes } from 'src/core/server/types'; -import { Datafeed, Job } from '../../public/jobs/new_job/common/job_creator/configs'; +import { Datafeed, Job } from '../../public/application/jobs/new_job/common/job_creator/configs'; export interface ModuleJob { id: string; diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.js b/x-pack/legacy/plugins/ml/common/util/job_utils.js index 90a00d40d17b1..999eb44b372bc 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.js @@ -12,7 +12,7 @@ import numeral from '@elastic/numeral'; import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation'; import { parseInterval } from './parse_interval'; import { maxLengthValidator } from './validators'; -import { CREATED_BY_LABEL } from '../../public/jobs/new_job/common/job_creator/util/constants'; +import { CREATED_BY_LABEL } from '../../public/application/jobs/new_job/common/job_creator/util/constants'; // work out the default frequency based on the bucket_span in seconds export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds) { diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 51f7b88315f85..3cafa232f0744 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -28,7 +28,7 @@ export const ml = (kibana: any) => { publicDir: resolve(__dirname, 'public'), uiExports: { - managementSections: ['plugins/ml/management'], + managementSections: ['plugins/ml/application/management'], app: { title: i18n.translate('xpack.ml.mlNavTitle', { defaultMessage: 'Machine Learning', @@ -36,12 +36,12 @@ export const ml = (kibana: any) => { description: i18n.translate('xpack.ml.mlNavDescription', { defaultMessage: 'Machine Learning for the Elastic Stack', }), - icon: 'plugins/ml/ml.svg', + icon: 'plugins/ml/application/ml.svg', euiIconType: 'machineLearningApp', - main: 'plugins/ml/app', + main: 'plugins/ml/application/app', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: ['plugins/ml/hacks/toggle_app_link_in_nav'], + hacks: ['plugins/ml/application/hacks/toggle_app_link_in_nav'], savedObjectSchemas: { 'ml-telemetry': { isNamespaceAgnostic: true, diff --git a/x-pack/legacy/plugins/ml/public/application/app.js b/x-pack/legacy/plugins/ml/public/application/app.js index ead1af5f64e07..722e2c8d05e9b 100644 --- a/x-pack/legacy/plugins/ml/public/application/app.js +++ b/x-pack/legacy/plugins/ml/public/application/app.js @@ -12,17 +12,17 @@ import 'ui/autoload/all'; // needed to make syntax highlighting work in ace editors import 'ace'; -import 'plugins/ml/access_denied'; -import 'plugins/ml/jobs'; -import 'plugins/ml/overview'; -import 'plugins/ml/services/calendar_service'; -import 'plugins/ml/data_frame_analytics'; -import 'plugins/ml/datavisualizer'; -import 'plugins/ml/explorer'; -import 'plugins/ml/timeseriesexplorer'; -import 'plugins/ml/components/navigation_menu'; -import 'plugins/ml/components/loading_indicator'; -import 'plugins/ml/settings'; +import './access_denied'; +import './jobs'; +import './overview'; +import './services/calendar_service'; +import './data_frame_analytics'; +import './datavisualizer'; +import './explorer'; +import './timeseriesexplorer'; +import './components/navigation_menu'; +import './components/loading_indicator'; +import './settings'; import uiRoutes from 'ui/routes'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx index 52224e5fc3fb6..3d98e2d66935c 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_description_list/index.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { EuiDescriptionList } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { Annotation } from '../../../../common/types/annotations'; +import { Annotation } from '../../../../../common/types/annotations'; import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx index 871dcd74d0907..7fa47f3518b81 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx @@ -10,7 +10,7 @@ import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.jso import React, { ComponentType } from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Annotation } from '../../../../common/types/annotations'; +import { Annotation } from '../../../../../common/types/annotations'; import { annotation$ } from '../../../services/annotations_service'; import { AnnotationFlyout } from './index'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx index 586e503632eb9..84c16360795ea 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx @@ -26,7 +26,7 @@ import { CommonProps } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { InjectedIntlProps } from 'react-intl'; import { toastNotifications } from 'ui/notify'; -import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../common/constants/annotations'; +import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../../common/constants/annotations'; import { annotation$, annotationsRefresh$, diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index ecd1c43eeb9ab..3e5afd3c1e7e7 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -41,8 +41,8 @@ import { addItemToRecentlyAccessed } from '../../../util/recently_accessed'; import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { mlTableService } from '../../../services/table_service'; -import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../common/constants/search'; -import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../../common/util/job_utils'; +import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search'; +import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../../../common/util/job_utils'; import { annotation$, annotationsRefresh$ } from '../../../services/annotations_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 32d30741f43da..1ed30c7e13727 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import jobConfig from '../../../../common/types/__mocks__/job_config_farequote'; +import jobConfig from '../../../../../common/types/__mocks__/job_config_farequote'; import mockAnnotations from './__mocks__/mock_annotations.json'; import './annotations_table.test.mocks'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts index fd3b04b2332d8..4a29fec03da85 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { chromeServiceMock } from '../../../../../../../../src/core/public/mocks'; +import { chromeServiceMock } from '../../../../../../../../../src/core/public/mocks'; jest.doMock('ui/new_platform', () => ({ npStart: { diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js index dda516c819bf4..f3913879ff12c 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.js @@ -30,7 +30,7 @@ import { getColumns } from './anomalies_table_columns'; import { AnomalyDetails } from './anomaly_details'; import { mlTableService } from '../../services/table_service'; -import { RuleEditorFlyout } from '../../components/rule_editor'; +import { RuleEditorFlyout } from '../rule_editor'; import { ml } from '../../services/ml_api_service'; import { INFLUENCERS_LIMIT, diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index be2a3e1f4223b..3e5d1e7acc450 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -30,7 +30,7 @@ import { InfluencersCell } from './influencers_cell'; import { LinksMenu } from './links_menu'; import { checkPermission } from '../../privilege/check_privilege'; import { mlFieldFormatService } from '../../services/field_format_service'; -import { isRuleSupported } from '../../../common/util/anomaly_utils'; +import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { formatValue } from '../../formatters/format_value'; import { INFLUENCERS_LIMIT, diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js index b1eada31fbf5b..bbbf7f704c614 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomaly_details.js @@ -35,8 +35,8 @@ import { getSeverity, showActualForFunction, showTypicalForFunction, -} from '../../../common/util/anomaly_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; +} from '../../../../common/util/anomaly_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; import { formatValue } from '../../formatters/format_value'; import { MAX_CHARS } from './anomalies_table_constants'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js index dfa12e34928d2..19cd77655f97c 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -22,11 +22,11 @@ import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../../../src/plugins/data/public'; import { checkPermission } from '../../privilege/check_privilege'; -import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search'; -import { isRuleSupported } from '../../../common/util/anomaly_utils'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; +import { isRuleSupported } from '../../../../common/util/anomaly_utils'; +import { parseInterval } from '../../../../common/util/parse_interval'; import { escapeDoubleQuotes } from '../kql_filter_bar/utils'; import { getFieldTypeFromMapping } from '../../services/mapping_service'; import { ml } from '../../services/ml_api_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx index e74d1a73b3332..2288106c6a8ed 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx @@ -6,8 +6,8 @@ import React, { FC, memo } from 'react'; import { EuiHealth, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; -import { getSeverityColor } from '../../../../common/util/anomaly_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../../../common/constants/multi_bucket_impact'; +import { getSeverityColor } from '../../../../../common/util/anomaly_utils'; interface SeverityCellProps { /** diff --git a/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx index ea9bc4f0f92ee..42a3e97509452 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx @@ -10,7 +10,7 @@ import { TooltipValueFormatter } from '@elastic/charts'; // TODO: Below import is temporary, use `react-use` lib instead. // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { useObservable } from '../../../../../../../src/plugins/kibana_react/public/util/use_observable'; +import { useObservable } from '../../../../../../../../src/plugins/kibana_react/public/util/use_observable'; import { chartTooltip$, ChartTooltipValue } from './chart_tooltip_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js index 502d6078ffd3b..9e6cffa21c5fa 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js +++ b/x-pack/legacy/plugins/ml/public/application/components/controls/select_severity/select_severity.js @@ -22,7 +22,7 @@ import { EuiText, } from '@elastic/eui'; -import { getSeverityColor } from '../../../../common/util/anomaly_utils'; +import { getSeverityColor } from '../../../../../common/util/anomaly_utils'; import { injectObservablesAsProps } from '../../../util/observable_utils'; const warningLabel = i18n.translate('xpack.ml.controls.selectSeverity.warningLabel', { defaultMessage: 'warning' }); diff --git a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js index b303ed9b7f008..9c4baacd9eec7 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js +++ b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.js @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { RecognizedResult } from './recognized_result'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from '../../services/ml_api_service'; export class DataRecognizer extends Component { constructor(props) { diff --git a/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js index 20c8e5310788d..c219aeb84772c 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js +++ b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.js @@ -12,7 +12,7 @@ import { EuiToolTip } from '@elastic/eui'; // don't use something like plugins/ml/../common // because it won't work with the jest tests import { getMLJobTypeAriaLabel } from '../../util/field_types_utils'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { i18n } from '@kbn/i18n'; export const FieldTypeIcon = ({ tooltipEnabled = false, type, needsAria = true }) => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js index 11e98549684ee..c21a173db4a85 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js @@ -8,7 +8,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { FieldTypeIcon } from './field_type_icon'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; describe('FieldTypeIcon', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js index 7794d3fd23497..6551996e2d194 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js +++ b/x-pack/legacy/plugins/ml/public/application/components/influencers_list/influencers_list.js @@ -22,8 +22,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { abbreviateWholeNumber } from 'plugins/ml/formatters/abbreviate_whole_number'; -import { getSeverity } from 'plugins/ml/../common/util/anomaly_utils'; +import { abbreviateWholeNumber } from '../../formatters/abbreviate_whole_number'; +import { getSeverity } from '../../../../common/util/anomaly_utils'; import { EntityCell } from '../entity_cell'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx index 545e9231699fd..7546121250013 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_message_icon/job_message_icon.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; -import { AuditMessageBase } from '../../../common/types/audit_message'; +import { AuditMessageBase } from '../../../../common/types/audit_message'; interface Props { message: AuditMessageBase; diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx index 08f9a4379559b..aedb8b6d17d06 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -12,7 +12,7 @@ import { formatDate } from '@elastic/eui/lib/services/format'; import { i18n } from '@kbn/i18n'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { JobMessage } from '../../../common/types/audit_message'; +import { JobMessage } from '../../../../common/types/audit_message'; import { JobIcon } from '../job_message_icon'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js index 13899be860428..7725cf5e59482 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.js @@ -10,7 +10,7 @@ import { PropTypes } from 'prop-types'; import moment from 'moment'; import { ml } from '../../services/ml_api_service'; -import { JobSelectorTable } from './job_selector_table/'; +import { JobSelectorTable } from './job_selector_table'; import { IdBadges } from './id_badges'; import { NewSelectionIdBadges } from './new_selection_id_badges'; import { timefilter } from 'ui/timefilter'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js index 2cb8a9da3dfdf..97f46c7cb59ea 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js @@ -9,7 +9,7 @@ import React from 'react'; import { PropTypes } from 'prop-types'; import { EuiBadge } from '@elastic/eui'; -import { tabColor } from '../../../../common/util/group_color_utils'; +import { tabColor } from '../../../../../common/util/group_color_utils'; import { i18n } from '@kbn/i18n'; export function JobSelectorBadge({ diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js index a754fbfab5ca6..76417984828d2 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js @@ -10,7 +10,7 @@ import React, { Fragment, useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; import { CustomSelectionTable } from '../custom_selection_table'; import { JobSelectorBadge } from '../job_selector_badge'; -import { TimeRangeBar } from '../timerange_bar/'; +import { TimeRangeBar } from '../timerange_bar'; import { EuiFlexGroup, diff --git a/x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js b/x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js index 830510df25659..d26fc296e9744 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js +++ b/x-pack/legacy/plugins/ml/public/application/components/message_call_out/message_call_out.js @@ -16,7 +16,7 @@ import { EuiCallOut } from '@elastic/eui'; // don't use something like plugins/ml/../common // because it won't work with the jest tests -import { MESSAGE_LEVEL } from '../../../common/constants/message_levels'; +import { MESSAGE_LEVEL } from '../../../../common/constants/message_levels'; function getCallOutAttributes(message, status) { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js index 88818a3c978b3..77d2ed4643fca 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__tests__/utils.js @@ -16,7 +16,7 @@ import { APPLIES_TO, OPERATOR, FILTER_TYPE, -} from '../../../../common/constants/detector_rule'; +} from '../../../../../common/constants/detector_rule'; describe('ML - rule editor utils', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js index 9ed6b2220f196..f5340c9fc2374 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.js @@ -21,7 +21,7 @@ import { EuiText, } from '@elastic/eui'; -import { ACTION } from '../../../common/constants/detector_rule'; +import { ACTION } from '../../../../common/constants/detector_rule'; import { FormattedMessage } from '@kbn/i18n/react'; export function ActionsSection({ diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js index 9ba85c5caddba..04e2c764e5ed9 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/actions_section.test.js @@ -9,7 +9,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ActionsSection } from './actions_section'; -import { ACTION } from '../../../common/constants/detector_rule'; +import { ACTION } from '../../../../common/constants/detector_rule'; describe('ActionsSection', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js index a69ed1d7a5583..aeadcb8bf58ad 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.js @@ -25,7 +25,7 @@ import { EuiFieldNumber, } from '@elastic/eui'; -import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; +import { APPLIES_TO, OPERATOR } from '../../../../common/constants/detector_rule'; import { appliesToText, operatorToText } from './utils'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js index 5dcbe845f1176..640f90744aa8e 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/condition_expression.test.js @@ -11,7 +11,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ConditionExpression } from './condition_expression'; -import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; +import { APPLIES_TO, OPERATOR } from '../../../../common/constants/detector_rule'; describe('ConditionExpression', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js index 8db05d4752e45..e2bb62fa03790 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/conditions_section.test.js @@ -12,7 +12,7 @@ import React from 'react'; import { ConditionsSection } from './conditions_section'; import { getNewConditionDefaults } from './utils'; -import { APPLIES_TO, OPERATOR } from '../../../common/constants/detector_rule'; +import { APPLIES_TO, OPERATOR } from '../../../../common/constants/detector_rule'; describe('ConditionsSectionExpression', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index 904185fccf3f3..1ccfa5b664f59 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -47,8 +47,8 @@ import { addItemToFilter, } from './utils'; -import { ACTION, CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../../../common/constants/detector_rule'; -import { getPartitioningFieldNames } from '../../../common/util/job_utils'; +import { ACTION, CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../../../../common/constants/detector_rule'; +import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { metadata } from 'ui/metadata'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js index 18fdf8887dc7a..fe7bf81b6aca2 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.js @@ -24,7 +24,7 @@ import { EuiSelect, } from '@elastic/eui'; -import { FILTER_TYPE } from '../../../common/constants/detector_rule'; +import { FILTER_TYPE } from '../../../../common/constants/detector_rule'; import { filterTypeToText } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js index 3acd46b5d6aef..68be030d7c28a 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_expression.test.js @@ -12,7 +12,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ScopeExpression } from './scope_expression'; -import { FILTER_TYPE } from '../../../common/constants/detector_rule'; +import { FILTER_TYPE } from '../../../../common/constants/detector_rule'; describe('ScopeExpression', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js index f3a6c57e1cab4..b7c961758fbf2 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/scope_section.test.js @@ -20,7 +20,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ScopeSection } from './scope_section'; -import { FILTER_TYPE } from '../../../common/constants/detector_rule'; +import { FILTER_TYPE } from '../../../../common/constants/detector_rule'; describe('ScopeSection', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js index 0df0ded392be2..48dd62b436852 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.js @@ -23,7 +23,7 @@ import { EuiText, } from '@elastic/eui'; -import { APPLIES_TO } from '../../../../common/constants/detector_rule'; +import { APPLIES_TO } from '../../../../../common/constants/detector_rule'; import { formatValue } from '../../../formatters/format_value'; import { getAppliesToValueFromAnomaly, diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js index 206acb65b4ba3..866f2ff40a6d4 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/edit_condition_link.test.js @@ -10,7 +10,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { EditConditionLink } from './edit_condition_link'; -import { APPLIES_TO } from '../../../../common/constants/detector_rule'; +import { APPLIES_TO } from '../../../../../common/constants/detector_rule'; function prepareTest(updateConditionValueFn, appliesTo) { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js index d9fc6b9ed64cf..c0553c32eaf24 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/rule_action_panel.test.js @@ -32,7 +32,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { RuleActionPanel } from './rule_action_panel'; -import { ACTION } from '../../../../common/constants/detector_rule'; +import { ACTION } from '../../../../../common/constants/detector_rule'; describe('RuleActionPanel', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js index 84f1c8207f3ff..a6e3950a75a01 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/utils.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ACTION, APPLIES_TO, FILTER_TYPE, OPERATOR } from '../../../common/constants/detector_rule'; +import { ACTION, APPLIES_TO, FILTER_TYPE, OPERATOR } from '../../../../common/constants/detector_rule'; import { cloneDeep } from 'lodash'; import { ml } from '../../services/ml_api_service'; import { mlJobService } from '../../services/job_service'; import { i18n } from '@kbn/i18n'; -import { processCreatedBy } from '../../../common/util/job_utils'; +import { processCreatedBy } from '../../../../common/util/job_utils'; export function getNewConditionDefaults() { return { diff --git a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js index a400e37e85219..50dc2b7f43f99 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/legacy/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -37,8 +37,8 @@ const jobTipsUrl = `https://www.elastic.co/guide/en/kibana/${metadata.branch}/jo // don't use something like plugins/ml/../common // because it won't work with the jest tests -import { VALIDATION_STATUS } from '../../../common/constants/validation'; -import { getMostSevereMessageStatus } from '../../../common/util/validation_utils'; +import { VALIDATION_STATUS } from '../../../../common/constants/validation'; +import { getMostSevereMessageStatus } from '../../../../common/util/validation_utils'; const defaultIconType = 'questionInCircle'; const getDefaultState = () => ({ diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts index 07979d7c1bd11..2e442c5c61b1e 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { searchSourceMock } from '../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; +import { searchSourceMock } from '../../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; export const savedSearchMock = { id: 'the-saved-search-id', diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts index 23de4c5b69ac7..fde854b7f41c3 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB } from '../breadcrumbs'; +import { ML_BREADCRUMB } from '../../breadcrumbs'; export function getDataFrameAnalyticsBreadcrumbs() { return [ diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index f910b8ea8a233..344a82f4d54d4 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -11,7 +11,7 @@ import { Subscription } from 'rxjs'; import { idx } from '@kbn/elastic-idx'; import { cloneDeep } from 'lodash'; import { ml } from '../../services/ml_api_service'; -import { Dictionary } from '../../../common/types/common'; +import { Dictionary } from '../../../../common/types/common'; import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form'; import { SavedSearchQuery } from '../../contexts/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx index 76df839b9346b..c41285f40d64b 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx @@ -14,7 +14,7 @@ const module = uiModules.get('apps/ml', ['react']); import { IndexPatterns } from 'ui/index_patterns'; import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../common/types/angular'; +import { InjectorService } from '../../../../../common/types/angular'; import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index e639f32116d4a..fc860251bf83d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { useRefreshAnalyticsList } from '../../../../common'; import { JobMessages } from '../../../../../components/job_messages'; -import { JobMessage } from '../../../../../../common/types/audit_message'; +import { JobMessage } from '../../../../../../../common/types/audit_message'; interface Props { analyticsId: string; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts index cfd900b303aa3..4ccfa8a562c6c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts @@ -11,7 +11,7 @@ import { timefilter } from 'ui/timefilter'; import { DEFAULT_REFRESH_INTERVAL_MS, MINIMUM_REFRESH_INTERVAL_MS, -} from '../../../../../../common/constants/jobs_list'; +} from '../../../../../../../common/constants/jobs_list'; import { useRefreshAnalyticsList } from '../../../../common'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx index 027acf6fa2e79..15f30b6cca6c4 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpers'; import { CreateAnalyticsButton } from './create_analytics_button'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx index 2bcc7305c5df7..880a1354e7a64 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpers'; import { CreateAnalyticsFlyout } from './create_analytics_flyout'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx index 846397aa93929..592b53dcecba0 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountHook } from '../../../../../../../../../../test_utils/enzyme_helpers'; import { CreateAnalyticsForm } from './create_analytics_form'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 598f88387f410..47af274424c44 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -22,7 +22,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { metadata } from 'ui/metadata'; import { IndexPattern, INDEX_PATTERN_ILLEGAL_CHARACTERS } from 'ui/index_patterns'; import { ml } from '../../../../../services/ml_api_service'; -import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields'; +import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useKibanaContext } from '../../../../../contexts/kibana'; @@ -32,7 +32,7 @@ import { DEFAULT_MODEL_MEMORY_LIMIT, getJobConfigFromFormState, } from '../../hooks/use_create_analytics_form/state'; -import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation'; +import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; import { Messages } from './messages'; import { JobType } from './job_type'; import { mmlUnitInvalidErrorMessage } from '../../hooks/use_create_analytics_form/reducer'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx index 63ddedaf65689..8299ff53393bb 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx @@ -13,7 +13,7 @@ const module = uiModules.get('apps/ml', ['react']); import { IndexPatterns } from 'ui/index_patterns'; import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../common/types/angular'; +import { InjectorService } from '../../../../../common/types/angular'; import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index f0fa2ad3b66db..56d09169a3c39 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -9,19 +9,19 @@ import { i18n } from '@kbn/i18n'; import { validateIndexPattern } from 'ui/index_patterns'; -import { isValidIndexName } from '../../../../../../common/util/es_utils'; +import { isValidIndexName } from '../../../../../../../common/util/es_utils'; import { Action, ACTION } from './actions'; import { getInitialState, getJobConfigFromFormState, State, JOB_TYPES } from './state'; import { isJobIdValid, validateModelMemoryLimitUnits, -} from '../../../../../../common/util/job_utils'; -import { maxLengthValidator } from '../../../../../../common/util/validators'; +} from '../../../../../../../common/util/job_utils'; +import { maxLengthValidator } from '../../../../../../../common/util/validators'; import { JOB_ID_MAX_LENGTH, ALLOWED_DATA_UNITS, -} from '../../../../../../common/constants/validation'; +} from '../../../../../../../common/constants/validation'; import { getDependentVar, isRegressionAnalysis } from '../../../../common/analytics'; const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index b90317015c8c9..f911b5a45e158 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DeepPartial } from '../../../../../../common/types/common'; +import { DeepPartial } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../../ml_nodes_check/check_ml_nodes'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts index 5534b37a6e427..a4d1fd37bc338 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../breadcrumbs'; +import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../breadcrumbs'; export function getDataVisualizerBreadcrumbs() { // Whilst top level nav menu with tabs remains, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts index 3e3f7e986b3d7..e8dd89f5db264 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../breadcrumbs'; +import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../../breadcrumbs'; export function getFileDataVisualizerBreadcrumbs() { // Whilst top level nav menu with tabs remains, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js index e1a5bfba3fd1e..c64a695772dde 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/fields_stats/fields_stats.js @@ -11,7 +11,7 @@ import React, { import { FieldStatsCard } from './field_stats_card'; import { getFieldNames } from './get_field_names'; -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; export class FieldsStats extends Component { diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js index 852f068ef419f..1f249dcdb0128 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/file_datavisualizer_view/file_datavisualizer_view.js @@ -23,7 +23,7 @@ import { ResultsView } from '../results_view'; import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts'; import { EditFlyout } from '../edit_flyout'; import { ImportView } from '../import_view'; -import { MAX_BYTES } from '../../../../../common/constants/file_datavisualizer'; +import { MAX_BYTES } from '../../../../../../common/constants/file_datavisualizer'; import { readFile, createUrlOverrides, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js index ac03bec8c2534..a3850b3def18d 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/importer/csv_importer.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public'; import { Importer } from './importer'; import Papa from 'papaparse'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index eefa9e461697e..3776245d90c81 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -12,7 +12,7 @@ import { KibanaConfigTypeFix } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; // @ts-ignore -import { FileDataVisualizerView } from './components/file_datavisualizer_view'; +import { FileDataVisualizerView } from './components/file_datavisualizer_view/index'; export interface FileDataVisualizerPageProps { indexPatterns: IndexPatterns; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx index 6a147aa1a991b..291e03a96e85f 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx @@ -17,7 +17,7 @@ import uiRoutes from 'ui/routes'; import { IndexPatterns } from 'ui/index_patterns'; import { KibanaConfigTypeFix } from '../../contexts/kibana'; import { getFileDataVisualizerBreadcrumbs } from './breadcrumbs'; -import { InjectorService } from '../../../common/types/angular'; +import { InjectorService } from '../../../../common/types/angular'; import { checkBasicLicense } from '../../license/check_license'; import { checkFindFileStructurePrivilege } from '../../privilege/check_privilege'; import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts index 03c66335ddb6c..aba45e04c638f 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts @@ -9,7 +9,7 @@ import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, // @ts-ignore -} from '../../breadcrumbs'; +} from '../../../breadcrumbs'; export function getDataVisualizerBreadcrumbs() { // Whilst top level nav menu with tabs remains, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts index 55b8f13631611..bf39cbb90e8f3 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/field_vis_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; // The internal representation of the configuration used to build the visuals // which display the field information. diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts index 83acda0419a0c..9a886cbc899c2 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/common/request.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; export interface FieldRequestConfig { fieldName?: string; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx index d162d166e8f6b..0493beed92482 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/field_data_card.tsx @@ -6,11 +6,11 @@ import React, { FC } from 'react'; -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { FieldVisConfig } from '../../common'; // @ts-ignore -import { FieldTitleBar } from '../../../../components/field_title_bar'; +import { FieldTitleBar } from '../../../../components/field_title_bar/index'; import { BooleanContent, DateContent, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx index 80a6f3d2d6743..6fd08076e1f46 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_types_select/field_types_select.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiSelect } from '@elastic/eui'; -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; interface Props { fieldTypes: ML_JOB_FIELD_TYPES[]; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx index f0b163c5c9e29..e32d1ca17e12e 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/fields_panel/fields_panel.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { toastNotifications } from 'ui/notify'; -import { ML_JOB_FIELD_TYPES } from '../../../../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../../../../common/constants/field_types'; import { FieldDataCard } from '../field_data_card'; import { FieldTypesSelect } from '../field_types_select'; import { FieldVisConfig } from '../../common'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx index 267be982dead4..a43b680720a2a 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx @@ -23,11 +23,11 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern } from 'ui/index_patterns'; -import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search'; import { SavedSearchQuery } from '../../../../contexts/kibana'; // @ts-ignore -import { KqlFilterBar } from '../../../../components/kql_filter_bar'; +import { KqlFilterBar } from '../../../../components/kql_filter_bar/index'; interface Props { indexPattern: IndexPattern; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index f0bb998a27614..fe0d69fdeec6b 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -10,7 +10,7 @@ import { toastNotifications } from 'ui/notify'; import { IndexPattern } from 'ui/index_patterns'; import { SavedSearchQuery } from '../../../contexts/kibana'; -import { IndexPatternTitle } from '../../../../common/types/kibana'; +import { IndexPatternTitle } from '../../../../../common/types/kibana'; import { ml } from '../../../services/ml_api_service'; import { FieldRequestConfig } from '../common'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx index df152b80c315e..58cd1c2c6fd0c 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx @@ -13,7 +13,7 @@ const module = uiModules.get('apps/ml', ['react']); import { I18nContext } from 'ui/i18n'; import { IndexPatterns } from 'ui/index_patterns'; -import { InjectorService } from '../../../common/types/angular'; +import { InjectorService } from '../../../../common/types/angular'; import { KibanaConfigTypeFix, KibanaContext } from '../../contexts/kibana/kibana_context'; import { createSearchItems } from '../../jobs/new_job/utils/new_job_utils'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index cf07fdf0ab2ca..1caa068620618 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -22,10 +22,10 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { KBN_FIELD_TYPES, esQuery } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES, esQuery } from '../../../../../../../../src/plugins/data/public'; import { NavigationMenu } from '../../components/navigation_menu'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; -import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search'; +import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isFullLicense } from '../../license/check_license'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js index 9f70505fc8dca..243adecaec78f 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.js @@ -5,7 +5,7 @@ */ -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs'; +import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js index 1acdd041c4052..985282df18f6a 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js @@ -41,7 +41,7 @@ import { getBoundsRoundedToInterval } from '../util/time_buckets'; import { getSelectedJobIds } from '../components/job_selector/job_select_service_utils'; import { InfluencersList } from '../components/influencers_list'; import { ALLOW_CELL_RANGE_SELECTION, dragSelect$, explorer$ } from './explorer_dashboard_service'; -import { mlResultsService } from 'plugins/ml/services/results_service'; +import { mlResultsService } from '../services/results_service'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; import { NavigationMenu } from '../components/navigation_menu'; import { CheckboxShowCharts, showCharts$ } from '../components/controls/checkbox_showcharts'; @@ -89,7 +89,7 @@ import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL, } from './explorer_constants'; -import { ML_RESULTS_INDEX_PATTERN } from '../../common/constants/index_patterns'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; // Explorer Charts import { ExplorerChartsContainer } from './explorer_charts/explorer_charts_container'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js index e62707d60a2a1..6d89d2de44498 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.js @@ -13,8 +13,8 @@ import _ from 'lodash'; -import { parseInterval } from '../../../common/util/parse_interval'; -import { getEntityFieldList } from '../../../common/util/anomaly_utils'; +import { parseInterval } from '../../../../common/util/parse_interval'; +import { getEntityFieldList } from '../../../../common/util/anomaly_utils'; import { buildConfigFromDetector } from '../../util/chart_config_builder'; import { mlJobService } from '../../services/job_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 588c3e3d6f1e9..1544e3a866001 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -21,7 +21,7 @@ import moment from 'moment'; // because it won't work with the jest tests import { formatHumanReadableDateTime } from '../../util/date_utils'; import { formatValue } from '../../formatters/format_value'; -import { getSeverityColor, getSeverityWithLow } from '../../../common/util/anomaly_utils'; +import { getSeverityColor, getSeverityWithLow } from '../../../../common/util/anomaly_utils'; import { getChartType, getTickValues, diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index be85af5a70c40..963da9e1d5efa 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -25,7 +25,7 @@ import { getSeverityColor, getSeverityWithLow, getMultiBucketImpactLabel, -} from '../../../common/util/anomaly_utils'; +} from '../../../../common/util/anomaly_utils'; import { LINE_CHART_ANOMALY_RADIUS, MULTI_BUCKET_SYMBOL_SIZE, diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 9d7e6e81e4896..b222b6e1160c6 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -19,8 +19,8 @@ import { getChartType } from '../../util/chart_utils'; -import { getEntityFieldList } from '../../../common/util/anomaly_utils'; -import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../../common/util/job_utils'; +import { getEntityFieldList } from '../../../../common/util/anomaly_utils'; +import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../../../common/util/job_utils'; import { mlResultsService } from '../../services/results_service'; import { mlJobService } from '../../services/job_service'; import { severity$ } from '../../components/controls/select_severity/select_severity'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js index 46e1b19edfed2..6d79975818870 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/index.js @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import 'plugins/ml/components/chart_tooltip'; +import '../../components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js index 3bedd90f05c37..c33b86bacf942 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_controller.js @@ -24,10 +24,10 @@ import { getAnomalyExplorerBreadcrumbs } from './breadcrumbs'; import { checkFullLicense } from '../license/check_license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; import { loadIndexPatterns } from '../util/index_utils'; -import { TimeBuckets } from 'plugins/ml/util/time_buckets'; +import { TimeBuckets } from '../util/time_buckets'; import { explorer$ } from './explorer_dashboard_service'; import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service'; -import { mlFieldFormatService } from 'plugins/ml/services/field_format_service'; +import { mlFieldFormatService } from '../services/field_format_service'; import { mlJobService } from '../services/job_service'; import { getSelectedJobIds, jobSelectServiceFactory } from '../components/job_selector/job_select_service_utils'; import { timefilter } from 'ui/timefilter'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js index 2ee725b6fda86..bd35241ff4e85 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_swimlane.js @@ -21,7 +21,7 @@ import moment from 'moment'; // because it won't work with the jest tests import { formatHumanReadableDateTime } from '../util/date_utils'; import { numTicksForDateFormat } from '../util/chart_utils'; -import { getSeverityColor } from '../../common/util/anomaly_utils'; +import { getSeverityColor } from '../../../common/util/anomaly_utils'; import { mlEscape } from '../util/string_utils'; import { mlChartTooltipService } from '../components/chart_tooltip/chart_tooltip_service'; import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js index 2f8b259b594bb..9c2d2041566e1 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js @@ -10,12 +10,12 @@ import { chain, each, get, union, uniq } from 'lodash'; -import { getEntityFieldList } from '../../common/util/anomaly_utils'; -import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../common/util/job_utils'; -import { parseInterval } from '../../common/util/parse_interval'; +import { getEntityFieldList } from '../../../common/util/anomaly_utils'; +import { isSourceDataChartableForDetector, isModelPlotEnabled } from '../../../common/util/job_utils'; +import { parseInterval } from '../../../common/util/parse_interval'; import { ml } from '../services/ml_api_service'; import { mlJobService } from '../services/job_service'; -import { mlResultsService } from 'plugins/ml/services/results_service'; +import { mlResultsService } from '../services/results_service'; import { MAX_CATEGORY_EXAMPLES, @@ -26,7 +26,7 @@ import { import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE -} from '../../common/constants/search'; +} from '../../../common/constants/search'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/index.js b/x-pack/legacy/plugins/ml/public/application/explorer/index.js index ba21c46308360..ebd3eb9c12662 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/index.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/index.js @@ -6,9 +6,9 @@ -import 'plugins/ml/explorer/explorer_controller'; -import 'plugins/ml/explorer/explorer_dashboard_service'; -import 'plugins/ml/explorer/explorer_react_wrapper_directive'; -import 'plugins/ml/explorer/explorer_charts'; -import 'plugins/ml/explorer/select_limit'; -import 'plugins/ml/components/job_selector'; +import '../explorer/explorer_controller'; +import '../explorer/explorer_dashboard_service'; +import '../explorer/explorer_react_wrapper_directive'; +import '../explorer/explorer_charts'; +import '../explorer/select_limit'; +import '../components/job_selector'; diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts index 5f146aef97fcc..bfed06a537a87 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.test.ts @@ -5,7 +5,7 @@ */ import moment from 'moment-timezone'; -import { AnomalyRecordDoc } from '../../common/types/anomalies'; +import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { formatValue } from './format_value'; describe('ML - formatValue formatter', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts index 9360957c4a911..abafe65615156 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/format_value.ts @@ -12,7 +12,7 @@ */ import moment from 'moment'; -import { AnomalyRecordDoc } from '../../common/types/anomalies'; +import { AnomalyRecordDoc } from '../../../common/types/anomalies'; const SIGFIGS_IF_ROUNDING = 3; // Number of sigfigs to use for values < 10 // Formats the value of an actual or typical field from a machine learning anomaly record. diff --git a/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js index 892d530c32735..7c6d8345736b5 100644 --- a/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js +++ b/x-pack/legacy/plugins/ml/public/application/hacks/toggle_app_link_in_nav.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; import { uiModules } from 'ui/modules'; import { npStart } from 'ui/new_platform'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts index 35e9c3326a4cc..f2954548ea547 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts @@ -10,7 +10,7 @@ import { ANOMALY_DETECTION_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB, -} from '../breadcrumbs'; +} from '../../breadcrumbs'; export function getJobManagementBreadcrumbs(): Breadcrumb[] { // Whilst top level nav menu with tabs remains, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js index 6edee63a07132..bbf3c3cfbadce 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/editor.js @@ -28,7 +28,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { isValidCustomUrlSettingsTimeRange } from '../../../jobs/components/custom_url_editor/utils'; +import { isValidCustomUrlSettingsTimeRange } from './utils'; import { isValidLabel } from '../../../util/custom_url_utils'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx index c23fdef324da7..ffb552da8ecf3 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx @@ -23,9 +23,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { isValidLabel, openCustomUrlWindow } from '../../../util/custom_url_utils'; import { getTestUrl } from './utils'; -import { parseInterval } from '../../../../common/util/parse_interval'; +import { parseInterval } from '../../../../../common/util/parse_interval'; import { TIME_RANGE_TYPE } from './constants'; -import { KibanaUrlConfig } from '../../../../common/types/custom_urls'; +import { KibanaUrlConfig } from '../../../../../common/types/custom_urls'; import { Job } from '../../new_job/common/job_creator/configs'; function isValidTimeRange(timeRange: KibanaUrlConfig['time_range']): boolean { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts index f8f618ae06762..ee9312aace119 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaUrlConfig } from '../../../../common/types/custom_urls'; +import { KibanaUrlConfig } from '../../../../../common/types/custom_urls'; import { Job } from '../../new_job/common/job_creator/configs'; export function getTestUrl(job: Job, customUrl: KibanaUrlConfig): Promise; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js index 83317b505d599..06391ad7895cb 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.js @@ -12,9 +12,9 @@ import { import chrome from 'ui/chrome'; import rison from 'rison-node'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; -import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; -import { parseInterval } from '../../../../common/util/parse_interval'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; +import { getPartitioningFieldNames } from '../../../../../common/util/job_utils'; +import { parseInterval } from '../../../../../common/util/parse_interval'; import { replaceTokensInUrlValue, isValidLabel } from '../../../util/custom_url_utils'; import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js index 716ece4b0e2dc..248096ffbd825 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js @@ -27,7 +27,7 @@ import { has } from 'lodash'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { parseInterval } from '../../../../../common/util/parse_interval'; +import { parseInterval } from '../../../../../../common/util/parse_interval'; import { ml } from '../../../../services/ml_api_service'; import { SelectSeverity } from '../../../../components/controls/select_severity/select_severity'; import { mlCreateWatchService } from './create_watch_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js index c45894c36b702..447869ff8fdb4 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/create_watch_flyout/watch.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../../common/constants/index_patterns'; export const watch = { trigger: { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js index 9d61d79b1d3e5..a9a81723244f9 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { deleteJobs } from '../utils'; -import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../common/constants/jobs_list'; +import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; export const DeleteJobModal = injectI18n(class extends Component { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js index 2b01a84894564..c1c98cddaf368 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js @@ -8,8 +8,8 @@ import { difference } from 'lodash'; import chrome from 'ui/chrome'; import { getNewJobLimits } from '../../../../services/ml_server_info'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { processCreatedBy } from '../../../../../common/util/job_utils'; +import { mlJobService } from '../../../../services/job_service'; +import { processCreatedBy } from '../../../../../../common/util/job_utils'; export function saveJob(job, newJobData, finish) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js index b09162b0e84cf..a1b4f82e79e66 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js @@ -18,9 +18,9 @@ import { EuiFieldNumber, } from '@elastic/eui'; -import { calculateDatafeedFrequencyDefaultSeconds } from 'plugins/ml/../common/util/job_utils'; +import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../common/util/job_utils'; import { getNewJobDefaults } from '../../../../../services/ml_server_info'; -import { parseInterval } from 'plugins/ml/../common/util/parse_interval'; +import { parseInterval } from '../../../../../../../common/util/parse_interval'; import { MLJobEditor } from '../../ml_job_editor'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js index 06fe88fcd8714..5c7d040ddbf27 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/detectors.js @@ -17,8 +17,8 @@ import { EuiSpacer, } from '@elastic/eui'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { detectorToString } from 'plugins/ml/util/string_utils'; +import { mlJobService } from '../../../../../services/job_service'; +import { detectorToString } from '../../../../../util/string_utils'; export class Detectors extends Component { constructor(props) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js index dbdc19d411481..3db5cb970315a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/job_details.js @@ -18,7 +18,7 @@ import { EuiComboBox, } from '@elastic/eui'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from '../../../../../services/ml_api_service'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; class JobDetailsUI extends Component { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js index b4bc018519170..a32bb3e1625d2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js @@ -17,9 +17,9 @@ import { EuiLoadingSpinner } from '@elastic/eui'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; -import { ML_DATA_PREVIEW_COUNT } from 'plugins/ml/../common/util/job_utils'; +import { mlJobService } from '../../../../services/job_service'; +import { checkPermission } from '../../../../privilege/check_privilege'; +import { ML_DATA_PREVIEW_COUNT } from '../../../../../../common/util/job_utils'; import { MLJobEditor } from '../ml_job_editor'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index 4586109512c1c..028e6a10d6abc 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -7,7 +7,7 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; import chrome from 'ui/chrome'; -import { detectorToString } from 'plugins/ml/util/string_utils'; +import { detectorToString } from '../../../../util/string_utils'; import { formatValues, filterObjects } from './format_values'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 369b9711ab938..3df869174c146 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -25,11 +25,11 @@ import { import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; import chrome from 'ui/chrome'; -import { FORECAST_REQUEST_STATE } from 'plugins/ml/../common/constants/states'; -import { addItemToRecentlyAccessed } from 'plugins/ml/util/recently_accessed'; -import { mlForecastService } from 'plugins/ml/services/forecast_service'; +import { FORECAST_REQUEST_STATE } from '../../../../../../../common/constants/states'; +import { addItemToRecentlyAccessed } from '../../../../../util/recently_accessed'; +import { mlForecastService } from '../../../../../services/forecast_service'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../../../../common/util/job_utils'; +import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob } from '../../../../../../../common/util/job_utils'; const MAX_FORECASTS = 500; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index 1d89650c51cae..7513ed355e544 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -7,7 +7,7 @@ import numeral from '@elastic/numeral'; import { formatDate } from '@elastic/eui/lib/services/format'; -import { toLocaleString } from 'plugins/ml/util/string_utils'; +import { toLocaleString } from '../../../../util/string_utils'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; const DATA_FORMAT = '0.0 b'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index ca80012767c2d..fbb64db94cd56 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -8,7 +8,7 @@ import React, { FC, useEffect, useState } from 'react'; import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; -import { JobMessage } from '../../../../../common/types/audit_message'; +import { JobMessage } from '../../../../../../common/types/audit_message'; interface JobMessagesPaneProps { jobId: string; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js index dabfc33e0f6af..60925434c35c7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js @@ -11,7 +11,7 @@ import React, { Fragment, } from 'react'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from '../../../../services/ml_api_service'; import { JobGroup } from '../job_group'; import { getSelectedJobIdFromUrl, clearSelectedJobIdFromUrl } from '../utils'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js index b0e10a975b863..d93f9ff7ff454 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_group/job_group.js @@ -5,7 +5,7 @@ */ -import { tabColor } from '../../../../../common/util/group_color_utils'; +import { tabColor } from '../../../../../../common/util/group_color_utils'; import PropTypes from 'prop-types'; import React from 'react'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index c160be429817e..fc07d4d2a0294 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -9,7 +9,7 @@ import { timefilter } from 'ui/timefilter'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ml } from '../../../../services/ml_api_service'; import { checkForAutoStartDatafeed, filterJobs, loadFullJob } from '../utils'; import { JobsList } from '../jobs_list'; import { JobDetails } from '../job_details'; @@ -30,7 +30,7 @@ import { DEFAULT_REFRESH_INTERVAL_MS, DELETING_JOBS_REFRESH_INTERVAL_MS, MINIMUM_REFRESH_INTERVAL_MS, -} from '../../../../../common/constants/jobs_list'; +} from '../../../../../../common/constants/jobs_list'; let jobsRefreshInterval = null; let deletingJobsRefreshTimeout = null; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js index 83116579a2adb..98f8180f4b980 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_stats_bar/jobs_stats_bar.js @@ -5,7 +5,7 @@ */ -import { JOB_STATE, DATAFEED_STATE } from 'plugins/ml/../common/constants/states'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../../common/constants/states'; import { StatsBar } from '../../../../components/stats_bar'; import PropTypes from 'prop-types'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index 499d911371d4a..6ba3f531dfb57 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -5,8 +5,8 @@ */ -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; -import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; +import { checkPermission } from '../../../../privilege/check_privilege'; +import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import PropTypes from 'prop-types'; import React, { Component, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index d104d2e02d07b..945f960f4aebe 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -5,7 +5,7 @@ */ -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; +import { checkPermission } from '../../../../../privilege/check_privilege'; import PropTypes from 'prop-types'; import React, { Component, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js index 381c9fb0f2671..a94161ee85836 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -6,8 +6,8 @@ -import { checkPermission } from 'plugins/ml/privilege/check_privilege'; -import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; +import { checkPermission } from '../../../../privilege/check_privilege'; +import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import React from 'react'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 08c8993585da2..2bb2fd310d175 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -6,14 +6,14 @@ import { each } from 'lodash'; import { toastNotifications } from 'ui/notify'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar'; +import { mlMessageBarService } from '../../../components/messagebar'; import rison from 'rison-node'; import chrome from 'ui/chrome'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import { JOB_STATE, DATAFEED_STATE } from 'plugins/ml/../common/constants/states'; -import { parseInterval } from '../../../../common/util/parse_interval'; +import { mlJobService } from '../../../services/job_service'; +import { ml } from '../../../services/ml_api_service'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; +import { parseInterval } from '../../../../../common/util/parse_interval'; import { i18n } from '@kbn/i18n'; export function loadFullJob(jobId) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js index e55075b0eb850..5fb1b93f7c564 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/validate_job.js @@ -12,7 +12,7 @@ import { validateModelMemoryLimit as validateModelMemoryLimitUtils, validateGroupNames as validateGroupNamesUtils, validateModelMemoryLimitUnits as validateModelMemoryLimitUnitsUtils, -} from '../../../../common/util/job_utils'; +} from '../../../../../common/util/job_utils'; import { isValidLabel, isValidTimeRange } from '../../../util/custom_url_utils'; export function validateModelMemoryLimit(mml) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js index 4b6f3f485d49d..f549ec3826cb5 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js @@ -11,12 +11,12 @@ import React from 'react'; import { uiModules } from 'ui/modules'; const module = uiModules.get('apps/ml', ['react']); -import { loadIndexPatterns } from 'plugins/ml/util/index_utils'; -import { checkFullLicense } from 'plugins/ml/license/check_license'; -import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { getMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; -import { getJobManagementBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; -import { loadMlServerInfo } from 'plugins/ml/services/ml_server_info'; +import { loadIndexPatterns } from '../../util/index_utils'; +import { checkFullLicense } from '../../license/check_license'; +import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; +import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; +import { getJobManagementBreadcrumbs } from '../../jobs/breadcrumbs'; +import { loadMlServerInfo } from '../../services/ml_server_info'; import uiRoutes from 'ui/routes'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts index f80d2e8e0fd55..502a88ecf6004 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts @@ -7,8 +7,8 @@ import memoizeOne from 'memoize-one'; import { isEqual } from 'lodash'; import { IndexPattern } from 'ui/index_patterns'; -import { IndexPatternTitle } from '../../../../../common/types/kibana'; -import { Field, SplitField, AggFieldPair } from '../../../../../common/types/fields'; +import { IndexPatternTitle } from '../../../../../../common/types/kibana'; +import { Field, SplitField, AggFieldPair } from '../../../../../../common/types/fields'; import { ml } from '../../../../services/ml_api_service'; import { mlResultsService } from '../../../../services/results_service'; import { getCategoryFields as getCategoryFieldsOrig } from './searches'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx index a71a264662fee..7211c034617f1 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx @@ -8,7 +8,7 @@ import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { Validation } from '../job_validator'; -import { tabColor } from '../../../../../common/util/group_color_utils'; +import { tabColor } from '../../../../../../common/util/group_color_utils'; import { Description } from '../../pages/components/job_details_step/components/groups/description'; export interface JobGroupsInputProps { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index 54e704767b992..22aebc2b88a88 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -8,12 +8,12 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/type import { IndexPattern } from 'ui/index_patterns'; import { JobCreator } from './job_creator'; -import { Field, Aggregation, SplitField } from '../../../../../common/types/fields'; +import { Field, Aggregation, SplitField } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector, CustomRule } from './configs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE } from './util/constants'; import { getRichDetectors } from './util/general'; -import { isValidJson } from '../../../../../common/util/validation_utils'; +import { isValidJson } from '../../../../../../common/util/validation_utils'; import { ml } from '../../../../services/ml_api_service'; export interface RichDetector { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts index 68ee45881586b..6c7493c5e52d3 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternTitle } from '../../../../../../common/types/kibana'; +import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; import { JobId } from './job'; export type DatafeedId = string; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index e5c6964f0f118..86a61e84b445c 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -6,17 +6,17 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; -import { IndexPatternTitle } from '../../../../../common/types/kibana'; -import { ML_JOB_AGGREGATION } from '../../../../../common/constants/aggregation_types'; -import { ES_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/public'; +import { IndexPatternTitle } from '../../../../../../common/types/kibana'; +import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; +import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { Job, Datafeed, Detector, JobId, DatafeedId, BucketSpan } from './configs'; -import { Aggregation, Field } from '../../../../../common/types/fields'; +import { Aggregation, Field } from '../../../../../../common/types/fields'; import { createEmptyJob, createEmptyDatafeed } from './util/default_configs'; import { mlJobService } from '../../../../services/job_service'; import { JobRunner, ProgressSubscriber } from '../job_runner'; import { JOB_TYPE, CREATED_BY_LABEL, SHARED_RESULTS_INDEX_NAME } from './util/constants'; import { isSparseDataJob } from './util/general'; -import { parseInterval } from '../../../../../common/util/parse_interval'; +import { parseInterval } from '../../../../../../common/util/parse_interval'; export class JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index 05a253a0962e9..fea328acb58b3 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -7,7 +7,12 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; import { JobCreator } from './job_creator'; -import { Field, Aggregation, SplitField, AggFieldPair } from '../../../../../common/types/fields'; +import { + Field, + Aggregation, + SplitField, + AggFieldPair, +} from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL, DEFAULT_MODEL_MEMORY_LIMIT } from './util/constants'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index ac7161422628d..9e9ccf8ab63e4 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -7,7 +7,12 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; import { JobCreator } from './job_creator'; -import { Field, Aggregation, SplitField, AggFieldPair } from '../../../../../common/types/fields'; +import { + Field, + Aggregation, + SplitField, + AggFieldPair, +} from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL } from './util/constants'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index aba3b08f330e8..5f3f6ff310d28 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -6,15 +6,15 @@ import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; -import { parseInterval } from '../../../../../common/util/parse_interval'; +import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JobCreator } from './job_creator'; -import { Field, Aggregation, AggFieldPair } from '../../../../../common/types/fields'; +import { Field, Aggregation, AggFieldPair } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector, BucketSpan } from './configs'; import { createBasicDetector } from './util/default_configs'; import { ML_JOB_AGGREGATION, ES_AGGREGATION, -} from '../../../../../common/constants/aggregation_types'; +} from '../../../../../../common/constants/aggregation_types'; import { JOB_TYPE, CREATED_BY_LABEL } from './util/constants'; import { getRichDetectors } from './util/general'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts index 2a09415c50bc4..1160401478ab7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts @@ -5,8 +5,8 @@ */ import { Job, Datafeed } from '../configs'; -import { IndexPatternTitle } from '../../../../../../common/types/kibana'; -import { Field, Aggregation, EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields'; +import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; +import { Field, Aggregation, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { Detector } from '../configs'; export function createEmptyJob(): Job { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 71535ab98c74f..a73c27d954afe 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -10,16 +10,16 @@ import { newJobCapsService } from '../../../../../services/new_job_capabilities_ import { ML_JOB_AGGREGATION, SPARSE_DATA_AGGREGATIONS, -} from '../../../../../../common/constants/aggregation_types'; -import { MLCATEGORY } from '../../../../../../common/constants/field_types'; +} from '../../../../../../../common/constants/aggregation_types'; +import { MLCATEGORY } from '../../../../../../../common/constants/field_types'; import { EVENT_RATE_FIELD_ID, Field, AggFieldPair, mlCategory, -} from '../../../../../../common/types/fields'; +} from '../../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; -import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../'; +import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../index'; import { CREATED_BY_LABEL, JOB_TYPE } from './constants'; const getFieldByIdFactory = (scriptFields: Field[]) => (id: string) => { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts index 4da87dedf14dd..9627d2e477528 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts @@ -9,7 +9,7 @@ import { ml } from '../../../../services/ml_api_service'; import { mlJobService } from '../../../../services/job_service'; import { JobCreator } from '../job_creator'; import { DatafeedId, JobId } from '../job_creator/configs'; -import { DATAFEED_STATE } from '../../../../../common/constants/states'; +import { DATAFEED_STATE } from '../../../../../../common/constants/states'; const REFRESH_INTERVAL_MS = 100; const TARGET_PROGRESS_DELTA = 2; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 82b1684b7b72f..550b579c93392 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -5,7 +5,10 @@ */ import { ReactElement } from 'react'; -import { basicJobValidation, basicDatafeedValidation } from '../../../../../common/util/job_utils'; +import { + basicJobValidation, + basicDatafeedValidation, +} from '../../../../../../common/util/job_utils'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { JobCreatorType } from '../job_creator'; import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts index b1bd352db387b..ab33afb23ef51 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts @@ -7,9 +7,12 @@ import { i18n } from '@kbn/i18n'; import { BasicValidations } from './job_validator'; import { Job, Datafeed } from '../job_creator/configs'; -import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +import { + ALLOWED_DATA_UNITS, + JOB_ID_MAX_LENGTH, +} from '../../../../../../common/constants/validation'; import { getNewJobLimits } from '../../../../services/ml_server_info'; -import { ValidationResults, ValidationMessage } from '../../../../../common/util/job_utils'; +import { ValidationResults, ValidationMessage } from '../../../../../../common/util/job_utils'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; export function populateValidationMessages( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index c9f78c9e8c096..d434e1be42e66 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -8,13 +8,13 @@ import { BehaviorSubject } from 'rxjs'; import { JobCreatorType, isMultiMetricJobCreator } from '../job_creator'; import { mlResultsService, ModelPlotOutputResults } from '../../../../services/results_service'; import { TimeBuckets } from '../../../../util/time_buckets'; -import { getSeverityType } from '../../../../../common/util/anomaly_utils'; -import { parseInterval } from '../../../../../common/util/parse_interval'; -import { ANOMALY_SEVERITY } from '../../../../../common/constants/anomalies'; +import { getSeverityType } from '../../../../../../common/util/anomaly_utils'; +import { parseInterval } from '../../../../../../common/util/parse_interval'; +import { ANOMALY_SEVERITY } from '../../../../../../common/constants/anomalies'; import { getScoresByRecord } from './searches'; import { JOB_TYPE } from '../job_creator/util/constants'; import { ChartLoader } from '../chart_loader'; -import { ES_AGGREGATION } from '../../../../../common/constants/aggregation_types'; +import { ES_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; export interface Results { progress: number; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts index 2e1b022a33b3f..724a6146854af 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; -import { ML_RESULTS_INDEX_PATTERN } from './../../../../../common/constants/index_patterns'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../../common/constants/index_patterns'; import { escapeForElasticsearchQuery } from '../../../../util/string_utils'; import { ml } from '../../../../services/ml_api_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx index 1fef1d804e6f2..c5188d045d84f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/charts/anomaly_chart/anomalies.tsx @@ -13,8 +13,8 @@ import React, { Fragment, FC } from 'react'; import { AnnotationDomainTypes, getAnnotationId, LineAnnotation } from '@elastic/charts'; import { Anomaly } from '../../../../common/results_loader'; -import { getSeverityColor } from '../../../../../../../common/util/anomaly_utils'; -import { ANOMALY_THRESHOLD } from '../../../../../../../common/constants/anomalies'; +import { getSeverityColor } from '../../../../../../../../common/util/anomaly_utils'; +import { ANOMALY_THRESHOLD } from '../../../../../../../../common/constants/anomalies'; interface Props { anomalyData?: Anomaly[]; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx index a284bd20b7ce1..7f5d2bfbe0e90 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx @@ -22,7 +22,7 @@ import { CombinedJob } from '../../../../common/job_creator/configs'; import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { JobCreatorContext } from '../../job_creator_context'; import { mlJobService } from '../../../../../../services/job_service'; -import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../common/util/job_utils'; +import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; const EDITOR_HEIGHT = '800px'; export enum EDITOR_MODE { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index cf26d6f532d0d..4815629ddd5c8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { Datafeed } from '../../../../common/job_creator/configs'; import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; -import { isValidJson } from '../../../../../../../common/util/validation_utils'; +import { isValidJson } from '../../../../../../../../common/util/validation_utils'; import { JobCreatorContext } from '../../job_creator_context'; const EDITOR_HEIGHT = '800px'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx index 924eacbc4b13c..6328366626894 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/frequency/frequency_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import { EuiFieldText } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Description } from './description'; -import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../common/util/job_utils'; +import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../../common/util/job_utils'; import { useStringifiedValue } from '../hooks'; export const FrequencyInput: FC = () => { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx index dfcedfe7c796e..fa1e7838f5938 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query/query_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; import { MLJobEditor } from '../../../../../../jobs_list/components/ml_job_editor'; import { Description } from './description'; -import { isValidJson } from '../../../../../../../../common/util/validation_utils'; +import { isValidJson } from '../../../../../../../../../common/util/validation_utils'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; const EDITOR_HEIGHT = '400px'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx index 3b1993f8e2c7e..f2e2516866835 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions } from '../../../../../common/job_creator/util/general'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts index 5fd3c98ed54c9..229fb8c3fd5f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_creator_context.ts @@ -5,7 +5,7 @@ */ import { createContext } from 'react'; -import { Field, Aggregation } from '../../../../../common/types/fields'; +import { Field, Aggregation } from '../../../../../../common/types/fields'; import { TimeBuckets } from '../../../../util/time_buckets'; import { JobCreatorType, SingleMetricJobCreator } from '../../common/job_creator'; import { ChartLoader } from '../../common/chart_loader'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx index d40b756857f46..cf0be9d3c0c4e 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../job_creator_context'; -import { tabColor } from '../../../../../../../../common/util/group_color_utils'; +import { tabColor } from '../../../../../../../../../common/util/group_color_utils'; import { Description } from './description'; export const GroupsInput: FC = () => { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index 5f93361982ea0..06c8068a9c005 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -26,7 +26,7 @@ import { Aggregation, EVENT_RATE_FIELD_ID, mlCategory, -} from '../../../../../../../../common/types/fields'; +} from '../../../../../../../../../common/types/fields'; import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; import { ModalWrapper } from './modal_wrapper'; import { detectorToString } from '../../../../../../../util/string_utils'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx index cb3dbb59f894a..c4b94c61ac4fb 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx @@ -9,7 +9,7 @@ import React, { Fragment, FC, useContext, useState } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { Aggregation, Field } from '../../../../../../../../common/types/fields'; +import { Aggregation, Field } from '../../../../../../../../../common/types/fields'; import { MetricSelector } from './metric_selector'; import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; import { DetectorList } from './detector_list'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx index 5bc38ca934165..104b629efd3cb 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx @@ -7,7 +7,7 @@ import React, { FC, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import { Aggregation, Field } from '../../../../../../../../common/types/fields'; +import { Aggregation, Field } from '../../../../../../../../../common/types/fields'; import { AdvancedDetectorModal, ModalPayload } from '../advanced_detector_modal'; import { RichDetector } from '../../../../../common/job_creator/advanced_job_creator'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx index bda29b244a5b1..a2434f3c33559 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext, useState, useEffect } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field, Aggregation, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { Field, Aggregation, AggFieldPair } from '../../../../../../../../../common/types/fields'; // The display label used for an aggregation e.g. sum(bytes). export type Label = string; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index f86baaccb84d3..4a1626ffcef89 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -7,7 +7,7 @@ import { useContext, useState } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; -import { EVENT_RATE_FIELD_ID } from '../../../../../../../../common/types/fields'; +import { EVENT_RATE_FIELD_ID } from '../../../../../../../../../common/types/fields'; import { isMultiMetricJobCreator, isPopulationJobCreator, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx index f9fdba31a0ad4..d995d40284aba 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions, createScriptFieldOptions, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx index feac3b30dfa3b..a44852b2ad625 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/detector_title/detector_title.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Field, Aggregation, SplitField } from '../../../../../../../../common/types/fields'; +import { Field, Aggregation, SplitField } from '../../../../../../../../../common/types/fields'; interface DetectorTitleProps { index: number; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index 293202415ced0..639bdb9ec76bf 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions, createScriptFieldOptions, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx index e4dd46b159a6c..b76fc120538f5 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx @@ -7,7 +7,7 @@ import React, { Fragment, FC } from 'react'; import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; -import { AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; +import { AggFieldPair, SplitField } from '../../../../../../../../../common/types/fields'; import { ChartSettings } from '../../../charts/common/settings'; import { LineChartData } from '../../../../../common/chart_loader'; import { ModelItem, Anomaly } from '../../../../../common/results_loader'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index b0a5049924cbc..ffa991388fbe2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -11,7 +11,7 @@ import { MultiMetricJobCreator } from '../../../../../common/job_creator'; import { LineChartData } from '../../../../../common/chart_loader'; import { DropDownLabel, DropDownProps } from '../agg_select'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { AggFieldPair } from '../../../../../../../../common/types/fields'; +import { AggFieldPair } from '../../../../../../../../../common/types/fields'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; import { ChartGrid } from './chart_grid'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx index 42bdc2a19deda..dd35dc136e70d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selector.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx index e3374be22485c..8cd533f8b2e29 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx @@ -7,7 +7,7 @@ import React, { Fragment, FC } from 'react'; import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { AggFieldPair, SplitField } from '../../../../../../../../common/types/fields'; +import { AggFieldPair, SplitField } from '../../../../../../../../../common/types/fields'; import { ChartSettings } from '../../../charts/common/settings'; import { LineChartData } from '../../../../../common/chart_loader'; import { ModelItem, Anomaly } from '../../../../../common/results_loader'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index 9a24381c9e35f..fe5d3ff0c29fb 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -12,7 +12,7 @@ import { PopulationJobCreator } from '../../../../../common/job_creator'; import { LineChartData } from '../../../../../common/chart_loader'; import { DropDownLabel, DropDownProps } from '../agg_select'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; import { SplitFieldSelector } from '../split_field'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx index b13f8e3a73a10..4474a2d6b5413 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx @@ -12,7 +12,7 @@ import { JobCreatorContext } from '../../../job_creator_context'; import { PopulationJobCreator } from '../../../../../common/job_creator'; import { Results, ModelItem, Anomaly } from '../../../../../common/results_loader'; import { LineChartData } from '../../../../../common/chart_loader'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { ChartGrid } from './chart_grid'; import { mlMessageBarService } from '../../../../../../../components/messagebar'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx index b831f9033f977..9857a585c14b8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selector.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; +import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; import { AggSelect, DropDownLabel, DropDownProps } from '../agg_select'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index 19ccca44dc0a5..f04b63f47789e 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -10,7 +10,7 @@ import { SingleMetricJobCreator } from '../../../../../common/job_creator'; import { LineChartData } from '../../../../../common/chart_loader'; import { AggSelect, DropDownLabel, DropDownProps, createLabel } from '../agg_select'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; -import { AggFieldPair } from '../../../../../../../../common/types/fields'; +import { AggFieldPair } from '../../../../../../../../../common/types/fields'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; import { getChartSettings } from '../../../charts/common/settings'; import { mlMessageBarService } from '../../../../../../../components/messagebar'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx index 76461e1306333..2884bce4d89ad 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/sparse_data/sparse_data_switch.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiSwitch } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Description } from './description'; -import { ES_AGGREGATION } from '../../../../../../../../common/constants/aggregation_types'; +import { ES_AGGREGATION } from '../../../../../../../../../common/constants/aggregation_types'; export const SparseDataSwitch: FC = () => { const { jobCreator, jobCreatorUpdated, jobCreatorUpdate } = useContext(JobCreatorContext); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx index b387d4a2fc34f..918163572076c 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -8,7 +8,7 @@ import React, { FC, memo, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { SplitField } from '../../../../../../../../common/types/fields'; +import { SplitField } from '../../../../../../../../../common/types/fields'; import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx index d08e989c49dea..fc78e8e244193 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/by_field.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { SplitFieldSelect } from './split_field_select'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service'; import { MultiMetricJobCreator, PopulationJobCreator } from '../../../../../common/job_creator'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx index 0dc76be9f8f07..378c088332ed4 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; -import { Field, SplitField } from '../../../../../../../../common/types/fields'; +import { Field, SplitField } from '../../../../../../../../../common/types/fields'; interface DropDownLabel { label: string; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx index 2f240344e0ea5..efe32e3173cad 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; -import { Field } from '../../../../../../../../common/types/fields'; +import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions, createScriptFieldOptions, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx index 5e1bf9f1ec889..c624972aa07ea 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiDescriptionList, EuiFormRow } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { MLJobEditor } from '../../../../../../jobs_list/components/ml_job_editor'; -import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../common/util/job_utils'; +import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../../common/util/job_utils'; import { DEFAULT_QUERY_DELAY } from '../../../../../common/job_creator/util/constants'; import { getNewJobDefaults } from '../../../../../../../services/ml_server_info'; import { ListItems, defaultLabel, Italic } from '../common'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx index a6ef18d4931b9..de019cbe86f9d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -12,9 +12,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { JobRunner } from '../../../../../common/job_runner'; // @ts-ignore -import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout'; -import { JobCreatorContext } from '../../../../components/job_creator_context'; -import { DATAFEED_STATE } from '../../../../../../../../common/constants/states'; +import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout/index'; +import { JobCreatorContext } from '../../../job_creator_context'; +import { DATAFEED_STATE } from '../../../../../../../../../common/constants/states'; interface Props { jobRunner: JobRunner | null; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js index bd63a16abfacd..ffa16930e79f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; // Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from 'plugins/ml/util/index_utils'; +import * as indexUtils from '../../../../../util/index_utils'; describe('ML - Index or Saved Search selection directive', () => { let $scope; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx index 7f3edf0896840..9bd653708d9c0 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx @@ -13,7 +13,7 @@ const module = uiModules.get('apps/ml', ['react']); import { timefilter } from 'ui/timefilter'; import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; +import { InjectorService } from '../../../../../../common/types/angular'; import { Page } from './page'; module.directive('mlIndexOrSearch', ($injector: InjectorService) => { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js index 5be526f2eb2c0..bdf65e3bafe96 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; // Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from 'plugins/ml/util/index_utils'; +import * as indexUtils from '../../../../../util/index_utils'; describe('ML - Job Type Directive', () => { let $scope; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx index 59dff64c1cd78..8d54ca65a2852 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx @@ -14,8 +14,8 @@ import { timefilter } from 'ui/timefilter'; import { IndexPatterns } from 'ui/index_patterns'; import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; -import { createSearchItems } from '../../../new_job/utils/new_job_utils'; +import { InjectorService } from '../../../../../../common/types/angular'; +import { createSearchItems } from '../../utils/new_job_utils'; import { Page } from './page'; import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx index 1725211861c0c..db4078ba1bbc8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx @@ -14,7 +14,7 @@ import { timefilter } from 'ui/timefilter'; import { IndexPatterns } from 'ui/index_patterns'; import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; +import { InjectorService } from '../../../../../../common/types/angular'; import { createSearchItems } from '../../utils/new_job_utils'; import { Page, PageProps } from './page'; import { JOB_TYPE } from '../../common/job_creator/util/constants'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts index 7f3f6c364e048..a527d92342d4c 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts @@ -17,7 +17,7 @@ import { getAdvancedJobConfigurationBreadcrumbs, } from '../../../breadcrumbs'; -import { Route } from '../../../../../common/types/kibana'; +import { Route } from '../../../../../../common/types/kibana'; import { loadNewJobCapabilities } from '../../../../services/new_job_capabilities_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js index 7cbf22bf45ec5..d5d5ee4438e32 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; // Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from 'plugins/ml/util/index_utils'; +import * as indexUtils from '../../../../util/index_utils'; describe('ML - Recognize job directive', () => { let $scope; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx index 7ec8cddfe3ed5..0dd222a1726ef 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx @@ -22,11 +22,11 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { ModuleJobUI } from '../page'; import { usePartialState } from '../../../../components/custom_hooks'; -import { composeValidators, maxLengthValidator } from '../../../../../common/util/validators'; -import { isJobIdValid } from '../../../../../common/util/job_utils'; -import { JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +import { composeValidators, maxLengthValidator } from '../../../../../../common/util/validators'; +import { isJobIdValid } from '../../../../../../common/util/job_utils'; +import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation'; import { JobGroupsInput } from '../../common/components'; -import { JobOverride } from '../../../../../common/types/modules'; +import { JobOverride } from '../../../../../../common/types/modules'; interface EditJobProps { job: ModuleJobUI; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx index ace8409734b74..2a15a42ba04f8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ModuleJobUI } from '../page'; import { SETUP_RESULTS_WIDTH } from './module_jobs'; -import { tabColor } from '../../../../../common/util/group_color_utils'; -import { JobOverride } from '../../../../../common/types/modules'; +import { tabColor } from '../../../../../../common/util/group_color_utils'; +import { JobOverride } from '../../../../../../common/types/modules'; interface JobItemProps { job: ModuleJobUI; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx index bae16d620af5b..4046bd8b09afa 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx @@ -25,8 +25,8 @@ import { composeValidators, maxLengthValidator, patternValidator, -} from '../../../../../common/util/validators'; -import { JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; +} from '../../../../../../common/util/validators'; +import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation'; import { usePartialState } from '../../../../components/custom_hooks'; import { TimeRange, TimeRangePicker } from '../../common/components'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx index adae037305ff8..7c72dc63691fa 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/components/module_jobs.tsx @@ -17,7 +17,7 @@ import { import { JobOverrides, ModuleJobUI, SAVE_STATE } from '../page'; import { JobItem } from './job_item'; import { EditJob } from './edit_job'; -import { JobOverride } from '../../../../../common/types/modules'; +import { JobOverride } from '../../../../../../common/types/modules'; interface ModuleJobsProps { jobs: ModuleJobUI[]; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx index 1882296f96418..2d08a1da07459 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx @@ -14,9 +14,9 @@ import { timefilter } from 'ui/timefilter'; import { IndexPatterns } from 'ui/index_patterns'; import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../common/types/angular'; +import { InjectorService } from '../../../../../common/types/angular'; -import { createSearchItems } from '../../new_job/utils/new_job_utils'; +import { createSearchItems } from '../utils/new_job_utils'; import { Page } from './page'; import { KibanaContext, KibanaConfigTypeFix } from '../../../contexts/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index f9a5230ef17d9..11b2a8f01342d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -33,7 +33,7 @@ import { KibanaObjectResponse, Module, ModuleJob, -} from '../../../../common/types/modules'; +} from '../../../../../common/types/modules'; import { mlJobService } from '../../../services/job_service'; import { CreateResultCallout } from './components/create_result_callout'; import { KibanaObjects } from './components/kibana_objects'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index cb4e7a21997e6..0e88b291e76fc 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -7,7 +7,7 @@ import { IndexPattern } from 'ui/index_patterns'; import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { esQuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { esQuery, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; export interface SearchItems { indexPattern: IIndexPattern; diff --git a/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js b/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js index e620a29e1d7f1..7d167fa066fda 100644 --- a/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js +++ b/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; -import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; +import { xpackInfo } from '../../../../../xpack_main/public/services/xpack_info'; +import { LICENSE_STATUS_VALID } from '../../../../../../common/constants/license_status'; import { xpackFeatureAvailable, } from '../check_license'; diff --git a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx index 8457e462567cc..c184a4d4e94e0 100644 --- a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx +++ b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { banners } from 'ui/notify'; import { EuiCallOut } from '@elastic/eui'; // @ts-ignore No declaration file for module -import { xpackInfo } from '../../../xpack_main/public/services/xpack_info'; -import { LICENSE_TYPE } from '../../common/constants/license'; -import { LICENSE_STATUS_VALID } from '../../../../common/constants/license_status'; +import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; +import { LICENSE_TYPE } from '../../../common/constants/license'; +import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; let licenseHasExpired = true; let licenseType: LICENSE_TYPE | null = null; diff --git a/x-pack/legacy/plugins/ml/public/application/management/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/_index.scss index d527197a5c2c6..e14df2d7c2039 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/_index.scss +++ b/x-pack/legacy/plugins/ml/public/application/management/_index.scss @@ -1 +1 @@ -@import './jobs_list/index'; +@import 'jobs_list/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/management/index.ts b/x-pack/legacy/plugins/ml/public/application/management/index.ts index 744b03c3d592b..092639cd5fbab 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/management/index.ts @@ -11,12 +11,12 @@ */ import { management } from 'ui/management'; -// @ts-ignore No declaration file for module -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { i18n } from '@kbn/i18n'; +// @ts-ignore No declaration file for module +import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; import { JOBS_LIST_PATH } from './management_urls'; -import { LICENSE_TYPE } from '../../common/constants/license'; -import 'plugins/ml/management/jobs_list'; +import { LICENSE_TYPE } from '../../../common/constants/license'; +import './jobs_list'; if ( xpackInfo.get('features.ml.showLinks', false) === true && diff --git a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss index 192091fb04e3c..841415620d691 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/_index.scss @@ -1 +1 @@ -@import './components/index'; +@import 'components/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss index 883ecd96745b4..b9e7d17ca209f 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/_index.scss @@ -1,4 +1,4 @@ -@import './jobs_list_page/stats_bar'; -@import './jobs_list_page/buttons'; -@import './jobs_list_page/expanded_row'; -@import './jobs_list_page/analytics_table'; +@import 'jobs_list_page/stats_bar'; +@import 'jobs_list_page/buttons'; +@import 'jobs_list_page/expanded_row'; +@import 'jobs_list_page/analytics_table'; diff --git a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index e3188c0892580..a19a27d00e9b0 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -21,7 +21,7 @@ import { import { metadata } from 'ui/metadata'; // @ts-ignore undeclared module -import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view'; +import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index'; import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; interface Props { diff --git a/x-pack/legacy/plugins/ml/public/application/overview/_index.scss b/x-pack/legacy/plugins/ml/public/application/overview/_index.scss index 192091fb04e3c..841415620d691 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/_index.scss +++ b/x-pack/legacy/plugins/ml/public/application/overview/_index.scss @@ -1 +1 @@ -@import './components/index'; +@import 'components/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts index 893ae5de450ad..9df503b462b6c 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore -import { ML_BREADCRUMB } from '../breadcrumbs'; +import { ML_BREADCRUMB } from '../../breadcrumbs'; export function getOverviewBreadcrumbs() { // Whilst top level nav menu with tabs remains, diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx index e865fd44c2a19..f638094cfb434 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx @@ -9,7 +9,7 @@ import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; // @ts-ignore no module file import { getLink } from '../../../jobs/jobs_list/components/job_actions/results'; -import { MlSummaryJobs } from '../../../../common/types/jobs'; +import { MlSummaryJobs } from '../../../../../common/types/jobs'; interface Props { jobsList: MlSummaryJobs; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 3c89e72ee4943..1f9d0413d45f9 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -20,8 +20,8 @@ import { toastNotifications } from 'ui/notify'; import { AnomalyDetectionTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils'; -import { Dictionary } from '../../../../common/types/common'; -import { MlSummaryJobs, MlSummaryJob } from '../../../../common/types/jobs'; +import { Dictionary } from '../../../../../common/types/common'; +import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/jobs'; export type GroupsDictionary = Dictionary; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx index 9c863f115685b..be96deec522ea 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx @@ -27,12 +27,12 @@ import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; import { ExplorerLink } from './actions'; import { getJobsFromGroup } from './utils'; import { GroupsDictionary, Group } from './anomaly_detection_panel'; -import { MlSummaryJobs } from '../../../../common/types/jobs'; +import { MlSummaryJobs } from '../../../../../common/types/jobs'; import { StatsBar, JobStatsBarStats } from '../../../components/stats_bar'; // @ts-ignore -import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge'; +import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge/index'; import { toLocaleString } from '../../../util/string_utils'; -import { getSeverityColor } from '../../../../common/util/anomaly_utils'; +import { getSeverityColor } from '../../../../../common/util/anomaly_utils'; // Used to pass on attribute names to table columns export enum AnomalyDetectionListColumns { diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts index db369d9228e6c..01848bad2670e 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_STATE, DATAFEED_STATE } from '../../../../common/constants/states'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { Group, GroupsDictionary } from './anomaly_detection_panel'; -import { MlSummaryJobs, MlSummaryJob } from '../../../../common/types/jobs'; +import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/jobs'; export function getGroupsFromJobs( jobs: MlSummaryJobs diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx index a285d5c91a266..8d2e4865ee6f4 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/content.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AnomalyDetectionPanel } from './anomaly_detection_panel'; -import { AnalyticsPanel } from './analytics_panel/'; +import { AnalyticsPanel } from './analytics_panel'; interface Props { createAnomalyDetectionJobDisabled: boolean; diff --git a/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts index 2c0cb71cd876c..6cc06231a08d0 100644 --- a/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts +++ b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { hasLicenseExpired } from '../license/check_license'; -import { Privileges, getDefaultPrivileges } from '../../common/types/privileges'; +import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; import { getPrivileges, getManageMlPrivileges } from './get_privileges'; import { ACCESS_DENIED_PATH } from '../management/management_urls'; diff --git a/x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts b/x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts index adca02b434e38..a3811779333d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts +++ b/x-pack/legacy/plugins/ml/public/application/privilege/get_privileges.ts @@ -7,7 +7,7 @@ import { ml } from '../services/ml_api_service'; import { setUpgradeInProgress } from '../services/upgrade_service'; -import { PrivilegesResponse } from '../../common/types/privileges'; +import { PrivilegesResponse } from '../../../common/types/privileges'; export function getPrivileges(): Promise { return new Promise((resolve, reject) => { diff --git a/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx index 3e2410292629b..eed9e46a47745 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.test.tsx @@ -6,7 +6,7 @@ import mockAnnotations from '../components/annotations/annotations_table/__mocks__/mock_annotations.json'; -import { Annotation } from '../../common/types/annotations'; +import { Annotation } from '../../../common/types/annotations'; import { annotation$, annotationsRefresh$ } from './annotations_service'; describe('annotations_service', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx index 4e8b0ad99d371..051c6ab445102 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx +++ b/x-pack/legacy/plugins/ml/public/application/services/annotations_service.tsx @@ -6,7 +6,7 @@ import { BehaviorSubject, Subject } from 'rxjs'; -import { Annotation } from '../../common/types/annotations'; +import { Annotation } from '../../../common/types/annotations'; /* A TypeScript helper type to allow a given component state attribute to be either an annotation or null. @@ -41,7 +41,7 @@ export type AnnotationState = Annotation | null; There are two ways to deal with updates of the observable: - 1. Inline subscription in an existing component. + 1. Inline subscription in an existing component. This requires the component to be a class component and manage its own state. - To react to an update, use `annotation$.subscribe(annotation => { })`. diff --git a/x-pack/legacy/plugins/ml/public/application/services/calendar_service.js b/x-pack/legacy/plugins/ml/public/application/services/calendar_service.js index 09e3001a0f7f5..dafb6b49ad14d 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/calendar_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/calendar_service.js @@ -9,9 +9,9 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar'; +import { ml } from '../services/ml_api_service'; +import { mlJobService } from '../services/job_service'; +import { mlMessageBarService } from '../components/messagebar'; diff --git a/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts b/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts index e4341adebda7a..ce6bc7896c44c 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/field_format_service.ts @@ -5,9 +5,9 @@ */ import { IndexPattern } from 'ui/index_patterns'; -import { mlFunctionToESAggregation } from '../../common/util/job_utils'; +import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; import { getIndexPatternById, getIndexPatternIdFromName } from '../util/index_utils'; -import { mlJobService } from '../services/job_service'; +import { mlJobService } from './job_service'; type FormatsByJobId = Record; type IndexPatternIdsByJob = Record; diff --git a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js index 4ec8a9dd1fec4..c420cca579c9c 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js @@ -10,7 +10,7 @@ // data on forecasts that have been performed. import _ from 'lodash'; -import { ML_RESULTS_INDEX_PATTERN } from '../../common/constants/index_patterns'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { ml } from './ml_api_service'; // Gets a basic summary of the most recently run forecasts for the specified diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js index 725de9e9c6237..fcd4d6088b44b 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js @@ -10,8 +10,8 @@ // Service for carrying out Elasticsearch queries to obtain data for the // Ml Results dashboards. -import { ML_NOTIFICATION_INDEX_PATTERN } from 'plugins/ml/../common/constants/index_patterns'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { ml } from '../services/ml_api_service'; // filter to match job_type: 'anomaly_detector' or no job_type field at all // if no job_type field exist, we can assume the message is for an anomaly detector job diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_service.js index 27dcd0135ad77..3db2b6c6dd88e 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/job_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/job_service.js @@ -15,8 +15,8 @@ import { ml } from './ml_api_service'; import { mlMessageBarService } from '../components/messagebar'; import { isWebUrl } from '../util/url_utils'; -import { ML_DATA_PREVIEW_COUNT } from '../../common/util/job_utils'; -import { parseInterval } from '../../common/util/parse_interval'; +import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; +import { parseInterval } from '../../../common/util/parse_interval'; const msgs = mlMessageBarService; let jobs = []; diff --git a/x-pack/legacy/plugins/ml/public/application/services/mapping_service.js b/x-pack/legacy/plugins/ml/public/application/services/mapping_service.js index c206930e12cd6..3ee49fda20819 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/mapping_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/mapping_service.js @@ -8,7 +8,7 @@ import _ from 'lodash'; -import { ml } from '../services/ml_api_service'; +import { ml } from './ml_api_service'; // Returns the mapping type of the specified field. // Accepts fieldName containing dots representing a nested sub-field. diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js index c889d0e98ad3e..560c4c460e118 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js @@ -8,7 +8,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js index 3f987a1763140..d29793366b9a2 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.js @@ -8,7 +8,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js index 323a70a6912b4..eb4c84ce5764c 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/datavisualizer.js @@ -6,7 +6,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js index 7f07e227e4167..18b7d93b0ca2e 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/filters.js @@ -9,7 +9,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index 12f39bfa78dc0..11c65851270eb 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Annotation } from '../../../common/types/annotations'; -import { AggFieldNamePair } from '../../../common/types/fields'; +import { Annotation } from '../../../../common/types/annotations'; +import { AggFieldNamePair } from '../../../../common/types/fields'; import { ExistingJobsAndGroups } from '../job_service'; -import { PrivilegesResponse } from '../../../common/types/privileges'; -import { MlSummaryJobs } from '../../../common/types/jobs'; -import { MlServerDefaults, MlServerLimits } from '../../services/ml_server_info'; -import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; +import { PrivilegesResponse } from '../../../../common/types/privileges'; +import { MlSummaryJobs } from '../../../../common/types/jobs'; +import { MlServerDefaults, MlServerLimits } from '../ml_server_info'; +import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { JobMessage } from '../../../common/types/audit_message'; +import { JobMessage } from '../../../../common/types/audit_message'; import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/analytics'; -import { DeepPartial } from '../../../common/types/common'; +import { DeepPartial } from '../../../../common/types/common'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js index 94c79fe470236..b3310eb6bcd53 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js @@ -9,7 +9,7 @@ import { pick } from 'lodash'; import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; import { annotations } from './annotations'; import { dataFrameAnalytics } from './data_frame_analytics'; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js index 39b646998b426..4ff1ca785d226 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js @@ -6,7 +6,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js index 7a776d61dca21..4bfec7643cecc 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js @@ -8,7 +8,7 @@ import chrome from 'ui/chrome'; -import { http } from '../../services/http_service'; +import { http } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); diff --git a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts index ded9aa410766e..a614be547abde 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts @@ -14,8 +14,8 @@ import { FieldId, NewJobCaps, EVENT_RATE_FIELD_ID, -} from '../../common/types/fields'; -import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; +} from '../../../common/types/fields'; +import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { ml } from './ml_api_service'; // called in the angular routing resolve block to initialize the diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service.js b/x-pack/legacy/plugins/ml/public/application/services/results_service.js index 640600159084d..b0840be4449bf 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/results_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service.js @@ -11,11 +11,11 @@ import _ from 'lodash'; // import d3 from 'd3'; -import { ML_MEDIAN_PERCENTS } from '../../common/util/job_utils'; +import { ML_MEDIAN_PERCENTS } from '../../../common/util/job_utils'; import { escapeForElasticsearchQuery } from '../util/string_utils'; -import { ML_RESULTS_INDEX_PATTERN } from '../../common/constants/index_patterns'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import { ml } from '../services/ml_api_service'; +import { ml } from './ml_api_service'; // Obtains the maximum bucket anomaly scores by job ID and time. // Pass an empty array or ['*'] to search over all job IDs. diff --git a/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts index 2cdfa5bfcf4d0..bd04003c9eca4 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS } from '../breadcrumbs'; +import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; export function getSettingsBreadcrumbs() { // Whilst top level nav menu with tabs remains, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js index fe1788db7f3fc..5754104b0e904 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/calendar_form.js @@ -23,7 +23,7 @@ import { } from '@elastic/eui'; import chrome from 'ui/chrome'; -import { EventsTable } from '../events_table/'; +import { EventsTable } from '../events_table'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js index e3ba5a4851fad..153860e73829e 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/imported_events/imported_events.js @@ -12,7 +12,7 @@ import { EuiText, EuiSpacer } from '@elastic/eui'; -import { EventsTable } from '../events_table/'; +import { EventsTable } from '../events_table'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 12c8339c52d71..feabd60d8d3a0 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -24,8 +24,8 @@ import { toastNotifications } from 'ui/notify'; import { NavigationMenu } from '../../../components/navigation_menu'; import { getCalendarSettingsData, validateCalendarId } from './utils'; -import { CalendarForm } from './calendar_form/'; -import { NewEventModal } from './new_event_modal/'; +import { CalendarForm } from './calendar_form'; +import { NewEventModal } from './new_event_modal'; import { ImportModal } from './import_modal'; import { ml } from '../../../services/ml_api_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index 2b554c2d46c1b..949e93bec76bc 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -28,7 +28,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import moment from 'moment'; -import { TIME_FORMAT } from '../events_table/'; +import { TIME_FORMAT } from '../events_table'; import { generateTempId } from '../utils'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js index ef7ea0c256296..d97a6f62c716a 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/utils.js @@ -7,7 +7,7 @@ import { ml } from '../../../services/ml_api_service'; -import { isJobIdValid } from '../../../../common/util/job_utils'; +import { isJobIdValid } from '../../../../../common/util/job_utils'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js index b7ad2c36f3b43..ef12a6ecc1618 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.js @@ -19,7 +19,7 @@ import { import { NavigationMenu } from '../../../components/navigation_menu'; import { CalendarsListHeader } from './header'; -import { CalendarsListTable } from './table/'; +import { CalendarsListTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { toastNotifications } from 'ui/notify'; import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js index 6303963ab3a14..a29487672ad90 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/utils.js @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; import { toastNotifications } from 'ui/notify'; -import { isJobIdValid } from 'plugins/ml/../common/util/job_utils'; -import { ml } from 'plugins/ml/services/ml_api_service'; +import { isJobIdValid } from '../../../../../common/util/job_utils'; +import { ml } from '../../../services/ml_api_service'; export function isValidFilterListId(id) { // Filter List ID requires the same format as a Job ID, therefore isJobIdValid can be used diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js index fd32a7c4d04b1..2aa4c845b125d 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js @@ -5,7 +5,7 @@ */ -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs'; +import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 00812d56ade4a..26fffb5e481ee 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -24,15 +24,15 @@ import { timefilter } from 'ui/timefilter'; // don't use something like plugins/ml/../common // because it won't work with the jest tests -import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../common/constants/states'; -import { MESSAGE_LEVEL } from '../../../../common/constants/message_levels'; -import { isJobVersionGte } from '../../../../common/util/job_utils'; -import { parseInterval } from '../../../../common/util/parse_interval'; +import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../../common/constants/states'; +import { MESSAGE_LEVEL } from '../../../../../common/constants/message_levels'; +import { isJobVersionGte } from '../../../../../common/util/job_utils'; +import { parseInterval } from '../../../../../common/util/parse_interval'; import { Modal } from './modal'; import { PROGRESS_STATES } from './progress_states'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import { mlJobService } from 'plugins/ml/services/job_service'; -import { mlForecastService } from 'plugins/ml/services/forecast_service'; +import { ml } from '../../../services/ml_api_service'; +import { mlJobService } from '../../../services/job_service'; +import { mlForecastService } from '../../../services/forecast_service'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js index dcde78e5e0b32..47051eecf9d04 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js @@ -25,7 +25,7 @@ import { } from '@elastic/eui'; -import { MessageCallOut } from 'plugins/ml/components/message_call_out'; +import { MessageCallOut } from '../../../components/message_call_out'; import { ForecastsList } from './forecasts_list'; import { RunControls } from './run_controls'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index 0b2a35fb3e39a..fef992719749e 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -28,11 +28,11 @@ import { // don't use something like plugins/ml/../common // because it won't work with the jest tests -import { JOB_STATE } from '../../../../common/constants/states'; +import { JOB_STATE } from '../../../../../common/constants/states'; import { FORECAST_DURATION_MAX_DAYS } from './forecasting_modal'; import { ForecastProgress } from './forecast_progress'; -import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; -import { checkPermission, createPermissionFailureMessage } from 'plugins/ml/privilege/check_privilege'; +import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; +import { checkPermission, createPermissionFailureMessage } from '../../../privilege/check_privilege'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts index 62ba8cfbe7d34..1f49ec1826422 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts @@ -6,8 +6,8 @@ import d3 from 'd3'; -import { Annotation } from '../../../../common/types/annotations'; -import { MlJob } from '../../../../common/types/jobs'; +import { Annotation } from '../../../../../common/types/annotations'; +import { MlJob } from '../../../../../common/types/jobs'; interface Props { selectedJob: MlJob; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 81852f025fb1f..5d621a51b710e 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -24,7 +24,7 @@ import chrome from 'ui/chrome'; import { getSeverityWithLow, getMultiBucketImpactLabel, -} from '../../../../common/util/anomaly_utils'; +} from '../../../../../common/util/anomaly_utils'; import { annotation$ } from '../../../services/annotations_service'; import { injectObservablesAsProps } from '../../../util/observable_utils'; import { formatValue } from '../../../formatters/format_value'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts index e668b39edd784..925107eb5f573 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts @@ -7,9 +7,9 @@ import d3 from 'd3'; import moment from 'moment'; -import { ANNOTATION_TYPE } from '../../../../common/constants/annotations'; -import { Annotation, Annotations } from '../../../../common/types/annotations'; -import { Dictionary } from '../../../../common/types/common'; +import { ANNOTATION_TYPE } from '../../../../../common/constants/annotations'; +import { Annotation, Annotations } from '../../../../../common/types/annotations'; +import { Dictionary } from '../../../../../common/types/common'; // @ts-ignore import { mlChartTooltipService } from '../../../components/chart_tooltip/chart_tooltip_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js index 520cce3c73260..5cbbd530c96f1 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js @@ -9,7 +9,7 @@ import _ from 'lodash'; import { ml } from '../services/ml_api_service'; -import { isModelPlotEnabled } from '../../common/util/job_utils'; +import { isModelPlotEnabled } from '../../../common/util/job_utils'; import { buildConfigFromDetector } from '../util/chart_config_builder'; import { mlResultsService } from '../services/results_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 1dec12a396578..8492ab11474f5 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -32,17 +32,17 @@ import { import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; -import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; +import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public'; -import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../common/constants/search'; -import { parseInterval } from '../../common/util/parse_interval'; +import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; +import { parseInterval } from '../../../common/util/parse_interval'; import { isModelPlotEnabled, isSourceDataChartableForDetector, isTimeSeriesViewJob, isTimeSeriesViewDetector, mlFunctionToESAggregation, -} from '../../common/util/job_utils'; +} from '../../../common/util/job_utils'; import { ChartTooltip } from '../components/chart_tooltip'; import { jobSelectServiceFactory, setGlobalState, getSelectedJobIds } from '../components/job_selector/job_select_service_utils'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js index fb741702841ed..61f5a76a5877e 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js @@ -18,12 +18,12 @@ import moment from 'moment-timezone'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE -} from '../../common/constants/search'; +} from '../../../common/constants/search'; import { isTimeSeriesViewJob, mlFunctionToESAggregation, -} from '../../common/util/job_utils'; -import { parseInterval } from '../../common/util/parse_interval'; +} from '../../../common/util/job_utils'; +import { parseInterval } from '../../../common/util/parse_interval'; import { ml } from '../services/ml_api_service'; import { mlForecastService } from '../services/forecast_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js index a6c8a9ed1ec17..9c107034359d6 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/util/__tests__/chart_utils.js @@ -17,7 +17,7 @@ import { showMultiBucketAnomalyMarker, showMultiBucketAnomalyTooltip, } from '../chart_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; +import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; import { CHART_TYPE } from '../../explorer/explorer_constants'; describe('ML - chart utils', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js b/x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js index 1529ea868d4e6..844d46001b8e7 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_config_builder.js @@ -13,7 +13,7 @@ import _ from 'lodash'; -import { mlFunctionToESAggregation } from '../../common/util/job_utils'; +import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; // Builds the basic configuration to plot a chart of the source data // analyzed by the the detector at the given index from the specified ML job. diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js index c73c89ad8c16c..8aa933eb5e53f 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js @@ -7,8 +7,8 @@ import d3 from 'd3'; -import { calculateTextWidth } from '../util/string_utils'; -import { MULTI_BUCKET_IMPACT } from '../../common/constants/multi_bucket_impact'; +import { calculateTextWidth } from './string_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; import moment from 'moment'; import rison from 'rison-node'; diff --git a/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts index ff97c3fffbf1c..6684ad3fa3e9b 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.test.ts @@ -10,12 +10,12 @@ import { isValidLabel, isValidTimeRange, } from './custom_url_utils'; -import { AnomalyRecordDoc } from '../../common/types/anomalies'; +import { AnomalyRecordDoc } from '../../../common/types/anomalies'; import { CustomUrlAnomalyRecordDoc, KibanaUrlConfig, UrlConfig, -} from '../../common/types/custom_urls'; +} from '../../../common/types/custom_urls'; describe('ML - custom URL utils', () => { const TEST_DOC: AnomalyRecordDoc = { diff --git a/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts index 0d21aed222cad..1a15f607e13c2 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/custom_url_utils.ts @@ -9,14 +9,14 @@ import { get, flow } from 'lodash'; import moment from 'moment'; -import { parseInterval } from '../../common/util/parse_interval'; +import { parseInterval } from '../../../common/util/parse_interval'; import { escapeForElasticsearchQuery, replaceStringTokens } from './string_utils'; import { UrlConfig, KibanaUrlConfig, CustomUrlAnomalyRecordDoc, -} from '../../common/types/custom_urls'; -import { AnomalyRecordDoc } from '../../common/types/anomalies'; +} from '../../../common/types/custom_urls'; +import { AnomalyRecordDoc } from '../../../common/types/anomalies'; // Value of custom_url time_range property indicating drilldown time range is calculated automatically // depending on the context in which the URL is being opened. diff --git a/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts index 3ca1adeb08f95..2abb8097598d2 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.test.ts @@ -5,8 +5,8 @@ */ import { FieldType } from 'ui/index_patterns'; -import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; -import { ML_JOB_FIELD_TYPES } from './../../common/constants/field_types'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; import { kbnTypeToMLJobType, getMLJobTypeAriaLabel, diff --git a/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts index e97ff11bc2bb7..e2b876aa8dbcd 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/field_types_utils.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { FieldType } from 'ui/index_patterns'; -import { ML_JOB_FIELD_TYPES } from './../../common/constants/field_types'; +import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; -import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; // convert kibana types to ML Job types // this is needed because kibana types only have string and not text and keyword. diff --git a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts index 5c15cdd6b8df0..f25821e8ca1ca 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; import chrome from 'ui/chrome'; -import { SavedSearchLoader } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; -import { start as data } from '../../../../../../src/legacy/core_plugins/data/public/legacy'; +import { SavedSearchLoader } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; +import { start as data } from '../../../../../../../src/legacy/core_plugins/data/public/legacy'; type IndexPatternSavedObject = SimpleSavedObject; diff --git a/x-pack/legacy/plugins/ml/public/application/util/ml_error.js b/x-pack/legacy/plugins/ml/public/application/util/ml_error.js index d5a3507ffaa15..2d319a395af54 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/ml_error.js +++ b/x-pack/legacy/plugins/ml/public/application/util/ml_error.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KbnError } from '../../../../../../src/plugins/kibana_utils/public'; +import { KbnError } from '../../../../../../../src/plugins/kibana_utils/public'; export class MLRequestFailure extends KbnError { // takes an Error object and and optional response object diff --git a/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx index bdc6c70aac749..7f1fc366bc5bb 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx +++ b/x-pack/legacy/plugins/ml/public/application/util/observable_utils.tsx @@ -7,7 +7,7 @@ import React, { Component, ComponentType } from 'react'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { Dictionary } from '../../common/types/common'; +import { Dictionary } from '../../../common/types/common'; // Sets up a ObservableComponent which subscribes to given observable updates and // and passes them on as prop values to the given WrappedComponent. diff --git a/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js index 98cb677a4851a..45181ab9f4f62 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js +++ b/x-pack/legacy/plugins/ml/public/application/util/time_buckets.js @@ -12,8 +12,8 @@ import chrome from 'ui/chrome'; import { npStart } from 'ui/new_platform'; import { timeBucketsCalcAutoIntervalProvider } from './calc_auto_interval'; -import { parseInterval } from '../../common/util/parse_interval'; -import { FIELD_FORMAT_IDS } from '../../../../../../src/plugins/data/public'; +import { parseInterval } from '../../../common/util/parse_interval'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/data/public'; const unitsDesc = dateMath.unitsDesc; const largeMax = unitsDesc.indexOf('w'); // Multiple units of week or longer converted to days for ES intervals. diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss index 97f0e037e2648..c3216773c1a32 100644 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ b/x-pack/legacy/plugins/ml/public/index.scss @@ -6,11 +6,11 @@ @import '@elastic/eui/src/components/panel/mixins'; // ML has it's own variables for coloring -@import 'variables'; +@import 'application/variables'; // Kibana management page ML section #kibanaManagementMLSection { - @import 'management/index'; + @import 'application/management/index'; } // Protect the rest of Kibana from ML generic namespacing @@ -18,33 +18,33 @@ #ml-app { // App level - @import 'app'; + @import 'application/app'; // Sub applications - @import 'data_frame_analytics/index'; - @import 'datavisualizer/index'; - @import 'explorer/index'; // SASSTODO: This file needs to be rewritten - @import 'jobs/index'; // SASSTODO: This collection of sass files has multiple problems - @import 'overview/index'; - @import 'settings/index'; - @import 'timeseriesexplorer/index'; + @import 'application/data_frame_analytics/index'; + @import 'application/datavisualizer/index'; + @import 'application/explorer/index'; // SASSTODO: This file needs to be rewritten + @import 'application/jobs/index'; // SASSTODO: This collection of sass files has multiple problems + @import 'application/overview/index'; + @import 'application/settings/index'; + @import 'application/timeseriesexplorer/index'; // Components - @import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly - @import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly - @import 'components/chart_tooltip/index'; - @import 'components/controls/index'; - @import 'components/entity_cell/index'; - @import 'components/field_title_bar/index'; - @import 'components/field_type_icon/index'; - @import 'components/influencers_list/index'; - @import 'components/items_grid/index'; - @import 'components/job_selector/index'; - @import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner - @import 'components/navigation_menu/index'; - @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly - @import 'components/stats_bar/index'; + @import 'application/components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly + @import 'application/components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly + @import 'application/components/chart_tooltip/index'; + @import 'application/components/controls/index'; + @import 'application/components/entity_cell/index'; + @import 'application/components/field_title_bar/index'; + @import 'application/components/field_type_icon/index'; + @import 'application/components/influencers_list/index'; + @import 'application/components/items_grid/index'; + @import 'application/components/job_selector/index'; + @import 'application/components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner + @import 'application/components/navigation_menu/index'; + @import 'application/components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly + @import 'application/components/stats_bar/index'; // Hacks are last so they can overwrite anything above if needed - @import 'hacks'; + @import 'application/hacks'; } diff --git a/x-pack/legacy/plugins/transform/public/shared_imports.ts b/x-pack/legacy/plugins/transform/public/shared_imports.ts index 35bcac16921b9..09af7b3c6f844 100644 --- a/x-pack/legacy/plugins/transform/public/shared_imports.ts +++ b/x-pack/legacy/plugins/transform/public/shared_imports.ts @@ -30,7 +30,7 @@ export { SortingPropType, SortDirection, SORT_DIRECTION, -} from '../../ml/public/components/ml_in_memory_table'; +} from '../../ml/public/application/components/ml_in_memory_table'; // @ts-ignore: could not find declaration file for module -export { KqlFilterBar } from '../../ml/public/components/kql_filter_bar'; +export { KqlFilterBar } from '../../ml/public/application/components/kql_filter_bar'; From b5133b5a57e8187ee612197e034127d57d464ffc Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Sun, 24 Nov 2019 08:36:11 +0100 Subject: [PATCH 040/128] [ML] Transform: Fix use of saved search in pivot wizard. (#51079) Fixes applying saved searches in the transform wizard. Previously, upon initializing the transform wizard's state, we would miss passing on the initialized data from kibanaContext. The resulting bug was that saved search were not applied in the generated transform config and source preview table. --- .../components/ml_in_memory_table/index.ts | 2 +- .../ml_in_memory_table/ml_in_memory_table.tsx | 58 ++-- .../components/ml_in_memory_table/types.ts | 77 +++--- .../components/exploration/exploration.tsx | 10 +- .../exploration/use_explore_data.ts | 2 +- .../regression_exploration/results_table.tsx | 10 +- .../use_explore_data.ts | 2 +- .../analytics_list/analytics_list.tsx | 4 +- .../components/analytics_panel/table.tsx | 8 +- .../anomaly_detection_panel/table.tsx | 6 +- .../transform/public/app/lib/kibana/index.ts | 4 +- .../public/app/lib/kibana/kibana_context.tsx | 41 ++- .../lib/kibana/use_current_index_pattern.ts | 2 +- .../source_index_preview.tsx | 11 +- .../step_create/step_create_form.tsx | 14 +- .../components/step_define/pivot_preview.tsx | 15 +- .../step_define/step_define_form.tsx | 23 +- .../step_define/step_define_summary.tsx | 10 +- .../step_define/use_pivot_preview_data.ts | 3 +- .../step_details/step_details_form.tsx | 81 +++--- .../components/wizard/wizard.tsx | 24 +- .../create_transform_section.tsx | 6 +- .../__snapshots__/expanded_row.test.tsx.snap | 15 +- .../expanded_row_details_pane.test.tsx.snap | 62 +++-- .../expanded_row_json_pane.test.tsx.snap | 50 ++-- .../components/transform_list/columns.tsx | 16 +- .../transform_list/expanded_row.tsx | 12 +- .../expanded_row_details_pane.tsx | 46 ++-- .../transform_list/expanded_row_json_pane.tsx | 28 +- .../expanded_row_messages_pane.tsx | 6 +- .../expanded_row_preview_pane.tsx | 45 +-- .../transform_list/transform_list.tsx | 4 +- .../transform_list/transform_table.tsx | 55 ++-- .../transform/public/shared_imports.ts | 2 +- ...{creation.ts => creation_index_pattern.ts} | 24 +- .../apps/transform/creation_saved_search.ts | 256 ++++++++++++++++++ .../test/functional/apps/transform/index.ts | 3 +- .../services/transform_ui/transform_table.ts | 77 ++++++ .../services/transform_ui/wizard.ts | 83 ++++++ 39 files changed, 849 insertions(+), 348 deletions(-) rename x-pack/test/functional/apps/transform/{creation.ts => creation_index_pattern.ts} (91%) create mode 100644 x-pack/test/functional/apps/transform/creation_saved_search.ts diff --git a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts index 91bf31ea1e7ab..bbd793696e005 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ProgressBar, MlInMemoryTable } from './ml_in_memory_table'; +export { ProgressBar, mlInMemoryTableFactory } from './ml_in_memory_table'; export * from './types'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx index d5316b22a6a6f..7caaadf65d6da 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/ml_in_memory_table.tsx @@ -71,34 +71,38 @@ const getInitialSorting = (columns: any, sorting: any) => { }; }; -import { MlInMemoryTableBasic } from './types'; - -export class MlInMemoryTable extends MlInMemoryTableBasic { - static getDerivedStateFromProps(nextProps: any, prevState: any) { - const derivedState = { - ...prevState.prevProps, - pageIndex: nextProps.pagination.initialPageIndex, - pageSize: nextProps.pagination.initialPageSize, - }; +import { mlInMemoryTableBasicFactory } from './types'; - if (nextProps.items !== prevState.prevProps.items) { - Object.assign(derivedState, { - prevProps: { - items: nextProps.items, - }, - }); - } +export function mlInMemoryTableFactory() { + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + + return class MlInMemoryTable extends MlInMemoryTableBasic { + static getDerivedStateFromProps(nextProps: any, prevState: any) { + const derivedState = { + ...prevState.prevProps, + pageIndex: nextProps.pagination.initialPageIndex, + pageSize: nextProps.pagination.initialPageSize, + }; - const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); - if ( - sortName !== prevState.prevProps.sortName || - sortDirection !== prevState.prevProps.sortDirection - ) { - Object.assign(derivedState, { - sortName, - sortDirection, - }); + if (nextProps.items !== prevState.prevProps.items) { + Object.assign(derivedState, { + prevProps: { + items: nextProps.items, + }, + }); + } + + const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); + if ( + sortName !== prevState.prevProps.sortName || + sortDirection !== prevState.prevProps.sortDirection + ) { + Object.assign(derivedState, { + sortName, + sortDirection, + }); + } + return derivedState; } - return derivedState; - } + }; } diff --git a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts index fac0309e0aeb6..49d831de47387 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts @@ -8,24 +8,21 @@ import { Component, HTMLAttributes, ReactElement, ReactNode } from 'react'; import { CommonProps, EuiInMemoryTable } from '@elastic/eui'; -// At some point this could maybe solved with a generic . -type Item = any; - // Not using an enum here because the original HorizontalAlignment is also a union type of string. type HorizontalAlignment = 'left' | 'center' | 'right'; -type SortableFunc = (item: Item) => any; -type Sortable = boolean | SortableFunc; +type SortableFunc = (item: T) => any; +type Sortable = boolean | SortableFunc; type DATA_TYPES = any; -type FooterFunc = (payload: { items: Item[]; pagination: any }) => ReactNode; +type FooterFunc = (payload: { items: T[]; pagination: any }) => ReactNode; type RenderFunc = (value: any, record?: any) => ReactNode; -export interface FieldDataColumnType { +export interface FieldDataColumnType { field: string; name: ReactNode; description?: string; dataType?: DATA_TYPES; width?: string; - sortable?: Sortable; + sortable?: Sortable; align?: HorizontalAlignment; truncateText?: boolean; render?: RenderFunc; @@ -34,38 +31,38 @@ export interface FieldDataColumnType { 'data-test-subj'?: string; } -export interface ComputedColumnType { +export interface ComputedColumnType { render: RenderFunc; name?: ReactNode; description?: string; - sortable?: (item: Item) => any; + sortable?: (item: T) => any; width?: string; truncateText?: boolean; 'data-test-subj'?: string; } type ICON_TYPES = any; -type IconTypesFunc = (item: Item) => ICON_TYPES; // (item) => oneOf(ICON_TYPES) +type IconTypesFunc = (item: T) => ICON_TYPES; // (item) => oneOf(ICON_TYPES) type BUTTON_ICON_COLORS = any; -type ButtonIconColorsFunc = (item: Item) => BUTTON_ICON_COLORS; // (item) => oneOf(ICON_BUTTON_COLORS) -interface DefaultItemActionType { +type ButtonIconColorsFunc = (item: T) => BUTTON_ICON_COLORS; // (item) => oneOf(ICON_BUTTON_COLORS) +interface DefaultItemActionType { type?: 'icon' | 'button'; name: string; description: string; - onClick?(item: Item): void; + onClick?(item: T): void; href?: string; target?: string; - available?(item: Item): boolean; - enabled?(item: Item): boolean; + available?(item: T): boolean; + enabled?(item: T): boolean; isPrimary?: boolean; - icon?: ICON_TYPES | IconTypesFunc; // required when type is 'icon' - color?: BUTTON_ICON_COLORS | ButtonIconColorsFunc; + icon?: ICON_TYPES | IconTypesFunc; // required when type is 'icon' + color?: BUTTON_ICON_COLORS | ButtonIconColorsFunc; } -interface CustomItemActionType { - render(item: Item, enabled: boolean): ReactNode; - available?(item: Item): boolean; - enabled?(item: Item): boolean; +interface CustomItemActionType { + render(item: T, enabled: boolean): ReactNode; + available?(item: T): boolean; + enabled?(item: T): boolean; isPrimary?: boolean; } @@ -76,20 +73,20 @@ export interface ExpanderColumnType { render: RenderFunc; } -type SupportedItemActionType = DefaultItemActionType | CustomItemActionType; +type SupportedItemActionType = DefaultItemActionType | CustomItemActionType; -export interface ActionsColumnType { - actions: SupportedItemActionType[]; +export interface ActionsColumnType { + actions: Array>; name?: ReactNode; description?: string; width?: string; } -export type ColumnType = - | ActionsColumnType - | ComputedColumnType +export type ColumnType = + | ActionsColumnType + | ComputedColumnType | ExpanderColumnType - | FieldDataColumnType; + | FieldDataColumnType; type QueryType = any; @@ -161,17 +158,17 @@ export interface OnTableChangeArg extends Sorting { page: { index: number; size: number }; } -type ItemIdTypeFunc = (item: Item) => string; +type ItemIdTypeFunc = (item: T) => string; type ItemIdType = | string // the name of the item id property | ItemIdTypeFunc; -export type EuiInMemoryTableProps = CommonProps & { - columns: ColumnType[]; +export type EuiInMemoryTableProps = CommonProps & { + columns: Array>; hasActions?: boolean; isExpandable?: boolean; isSelectable?: boolean; - items?: Item[]; + items?: T[]; loading?: boolean; message?: HTMLAttributes; error?: string; @@ -184,16 +181,18 @@ export type EuiInMemoryTableProps = CommonProps & { responsive?: boolean; selection?: SelectionType; itemId?: ItemIdType; - itemIdToExpandedRowMap?: Record; - rowProps?: (item: Item) => void | Record; + itemIdToExpandedRowMap?: Record; + rowProps?: (item: T) => void | Record; cellProps?: () => void | Record; onTableChange?: (arg: OnTableChangeArg) => void; }; -interface ComponentWithConstructor extends Component { +type EuiInMemoryTableType = typeof EuiInMemoryTable; + +interface ComponentWithConstructor extends EuiInMemoryTableType { new (): Component; } -export const MlInMemoryTableBasic = (EuiInMemoryTable as any) as ComponentWithConstructor< - EuiInMemoryTableProps ->; +export function mlInMemoryTableBasicFactory() { + return EuiInMemoryTable as ComponentWithConstructor>; +} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index fea4c861551a3..c4bba08353d84 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -35,7 +35,7 @@ import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; import { ColumnType, - MlInMemoryTableBasic, + mlInMemoryTableBasicFactory, OnTableChangeArg, SortingPropType, SORT_DIRECTION, @@ -59,7 +59,7 @@ import { } from '../../../../common'; import { getOutlierScoreFieldName } from './common'; -import { useExploreData } from './use_explore_data'; +import { useExploreData, TableItem } from './use_explore_data'; import { DATA_FRAME_TASK_STATE, Query as QueryType, @@ -167,7 +167,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { docFieldsCount = docFields.length; } - const columns: ColumnType[] = []; + const columns: Array> = []; if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { // table cell color coding takes into account: @@ -188,7 +188,7 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { columns.push( ...selectedFields.sort(sortColumns(tableItems[0], jobConfig.dest.results_field)).map(k => { - const column: ColumnType = { + const column: ColumnType = { field: k, name: k, sortable: true, @@ -425,6 +425,8 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { }); } + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return ( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts index a0728e0bae446..e76cbaa463f1d 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/use_explore_data.ts @@ -27,7 +27,7 @@ import { import { getOutlierScoreFieldName } from './common'; import { SavedSearchQuery } from '../../../../../contexts/kibana'; -type TableItem = Record; +export type TableItem = Record; interface LoadExploreDataArg { field: string; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index ec504492e0a5e..37c2e40c89c3c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -30,7 +30,7 @@ import { Query as QueryType } from '../../../analytics_management/components/ana import { ColumnType, - MlInMemoryTableBasic, + mlInMemoryTableBasicFactory, OnTableChangeArg, SortingPropType, SORT_DIRECTION, @@ -55,7 +55,7 @@ import { import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData } from './use_explore_data'; +import { useExploreData, TableItem } from './use_explore_data'; import { ExplorationTitle } from './regression_exploration'; const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; @@ -108,12 +108,12 @@ export const ResultsTable: FC = React.memo( docFieldsCount = docFields.length; } - const columns: ColumnType[] = []; + const columns: Array> = []; if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { columns.push( ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => { - const column: ColumnType = { + const column: ColumnType = { field: k, name: k, sortable: true, @@ -363,6 +363,8 @@ export const ResultsTable: FC = React.memo( ? errorMessage : searchError; + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return ( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts index bf3565abd8de4..3a83ad238d0e1 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts @@ -31,7 +31,7 @@ import { SearchQuery, } from '../../../../common'; -type TableItem = Record; +export type TableItem = Record; interface LoadExploreDataArg { field: string; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 6de278cda16e6..f98ce486f7337 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -34,7 +34,7 @@ import { getColumns } from './columns'; import { ExpandedRow } from './expanded_row'; import { ProgressBar, - MlInMemoryTable, + mlInMemoryTableFactory, OnTableChangeArg, SortDirection, SORT_DIRECTION, @@ -326,6 +326,8 @@ export const DataFrameAnalyticsList: FC = ({ setSortDirection(direction); }; + const MlInMemoryTable = mlInMemoryTableFactory(); + return ( diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx index 7ee9cff107db8..156e53b19874f 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/analytics_panel/table.tsx @@ -8,7 +8,7 @@ import React, { FC, useState } from 'react'; import { EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { - MlInMemoryTable, + mlInMemoryTableFactory, SortDirection, SORT_DIRECTION, OnTableChangeArg, @@ -27,7 +27,7 @@ import { AnalyticsViewAction } from '../../../data_frame_analytics/pages/analyti import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; interface Props { - items: any[]; + items: DataFrameAnalyticsListRow[]; } export const AnalyticsTable: FC = ({ items }) => { const [pageIndex, setPageIndex] = useState(0); @@ -37,7 +37,7 @@ export const AnalyticsTable: FC = ({ items }) => { const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); // id, type, status, progress, created time, view icon - const columns: ColumnType[] = [ + const columns: Array> = [ { field: DataFrameAnalyticsListColumn.id, name: i18n.translate('xpack.ml.overview.analyticsList.id', { defaultMessage: 'ID' }), @@ -113,6 +113,8 @@ export const AnalyticsTable: FC = ({ items }) => { }, }; + const MlInMemoryTable = mlInMemoryTableFactory(); + return ( = ({ items, jobsList, statsBarData const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); // columns: group, max anomaly, jobs in group, latest timestamp, docs processed, action to explorer - const columns: ColumnType[] = [ + const columns: Array> = [ { field: AnomalyDetectionListColumns.id, name: i18n.translate('xpack.ml.overview.anomalyDetection.tableId', { @@ -195,6 +195,8 @@ export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData }, }; + const MlInMemoryTable = mlInMemoryTableFactory(); + return ( diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts index 08cf7d0046e97..82d5362e21c02 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/index.ts @@ -5,10 +5,12 @@ */ export { - isKibanaContextInitialized, + useKibanaContext, + InitializedKibanaContextValue, KibanaContext, KibanaContextValue, SavedSearchQuery, + RenderOnlyWithInitializedKibanaContext, } from './kibana_context'; export { KibanaProvider } from './kibana_provider'; export { useCurrentIndexPattern } from './use_current_index_pattern'; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx index 2e4f0dfd4696a..e3515991e7bb1 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createContext } from 'react'; +import React, { createContext, useContext, FC } from 'react'; import { IndexPattern as IndexPatternType, @@ -15,14 +15,15 @@ import { SavedSearch } from '../../../../../../../../src/legacy/core_plugins/kib import { KibanaConfig } from '../../../../../../../../src/legacy/server/kbn_server'; // set() method is missing in original d.ts -export interface KibanaConfigTypeFix extends KibanaConfig { +interface KibanaConfigTypeFix extends KibanaConfig { set(key: string, value: any): void; } interface UninitializedKibanaContextValue { initialized: boolean; } -interface InitializedKibanaContextValue { + +export interface InitializedKibanaContextValue { combinedQuery: any; currentIndexPattern: IndexPatternType; currentSavedSearch: SavedSearch; @@ -41,3 +42,37 @@ export function isKibanaContextInitialized(arg: any): arg is InitializedKibanaCo export type SavedSearchQuery = object; export const KibanaContext = createContext({ initialized: false }); + +/** + * Custom hook to get the current kibanaContext. + * + * @remarks + * This hook should only be used in components wrapped in `RenderOnlyWithInitializedKibanaContext`, + * otherwise it will throw an error when KibanaContext hasn't been initialized yet. + * In return you get the benefit of not having to check if it's been initialized in the component + * where it's used. + * + * @returns `kibanaContext` + */ +export const useKibanaContext = () => { + const kibanaContext = useContext(KibanaContext); + + if (!isKibanaContextInitialized(kibanaContext)) { + throw new Error('useKibanaContext: kibanaContext not initialized'); + } + + return kibanaContext; +}; + +/** + * Wrapper component to render children only if `kibanaContext` has been initialized. + * In combination with `useKibanaContext` this avoids having to check for the initialization + * in consuming components. + * + * @returns `children` or `null` depending on whether `kibanaContext` is initialized or not. + */ +export const RenderOnlyWithInitializedKibanaContext: FC = ({ children }) => { + const kibanaContext = useContext(KibanaContext); + + return isKibanaContextInitialized(kibanaContext) ? <>{children} : null; +}; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/use_current_index_pattern.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/use_current_index_pattern.ts index e4b0725c324b4..12c5bde171b8b 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/use_current_index_pattern.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/use_current_index_pattern.ts @@ -12,7 +12,7 @@ export const useCurrentIndexPattern = () => { const context = useContext(KibanaContext); if (!isKibanaContextInitialized(context)) { - throw new Error('currentIndexPattern is undefined'); + throw new Error('useCurrentIndexPattern: kibanaContext not initialized'); } return context.currentIndexPattern; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx index 289ea62a57ab1..2b7d36cada3c6 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx @@ -31,7 +31,7 @@ import { import { ColumnType, - MlInMemoryTableBasic, + mlInMemoryTableBasicFactory, SortingPropType, SORT_DIRECTION, } from '../../../../../shared_imports'; @@ -183,8 +183,8 @@ export const SourceIndexPreview: React.FC = React.memo(({ cellClick, quer docFieldsCount = docFields.length; } - const columns: ColumnType[] = selectedFields.map(k => { - const column: ColumnType = { + const columns: Array> = selectedFields.map(k => { + const column: ColumnType = { field: `_source["${k}"]`, name: k, sortable: true, @@ -319,6 +319,8 @@ export const SourceIndexPreview: React.FC = React.memo(({ cellClick, quer defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', }); + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return ( @@ -410,6 +412,9 @@ export const SourceIndexPreview: React.FC = React.memo(({ cellClick, quer itemId="_id" itemIdToExpandedRowMap={itemIdToExpandedRowMap} isExpandable={true} + rowProps={item => ({ + 'data-test-subj': `transformSourceIndexPreviewRow row-${item._id}`, + })} sorting={sorting} /> )} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 9ab3edbe8ad6b..2ca3253d72b44 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { toastNotifications } from 'ui/notify'; @@ -32,7 +32,7 @@ import { import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { ToastNotificationText } from '../../../../components'; import { useApi } from '../../../../hooks/use_api'; -import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana'; +import { useKibanaContext } from '../../../../lib/kibana'; import { RedirectToTransformManagement } from '../../../../common/navigation'; import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; @@ -73,7 +73,7 @@ export const StepCreateForm: FC = React.memo( undefined ); - const kibanaContext = useContext(KibanaContext); + const kibanaContext = useKibanaContext(); useEffect(() => { onChange({ created, started, indexPatternId }); @@ -83,10 +83,6 @@ export const StepCreateForm: FC = React.memo( const api = useApi(); - if (!isKibanaContextInitialized(kibanaContext)) { - return null; - } - async function createTransform() { setCreated(true); @@ -151,8 +147,8 @@ export const StepCreateForm: FC = React.memo( } async function createAndStartTransform() { - const success = await createTransform(); - if (success) { + const acknowledged = await createTransform(); + if (acknowledged) { await startTransform(); } } diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx index ad7ef04c39760..3f4c7e21d3947 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx @@ -21,7 +21,11 @@ import { EuiTitle, } from '@elastic/eui'; -import { ColumnType, MlInMemoryTableBasic, SORT_DIRECTION } from '../../../../../shared_imports'; +import { + ColumnType, + mlInMemoryTableBasicFactory, + SORT_DIRECTION, +} from '../../../../../shared_imports'; import { dictionaryToArray } from '../../../../../../common/types/common'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; @@ -38,7 +42,7 @@ import { } from '../../../../common'; import { getPivotPreviewDevConsoleStatement } from './common'; -import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; +import { PreviewItem, PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; function sortColumns(groupByArr: PivotGroupByConfig[]) { return (a: string, b: string) => { @@ -210,7 +214,7 @@ export const PivotPreview: FC = React.memo(({ aggs, groupBy, columnKeys.sort(sortColumns(groupByArr)); const columns = columnKeys.map(k => { - const column: ColumnType = { + const column: ColumnType = { field: k, name: k, sortable: true, @@ -256,6 +260,8 @@ export const PivotPreview: FC = React.memo(({ aggs, groupBy, }, }; + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return ( @@ -273,6 +279,9 @@ export const PivotPreview: FC = React.memo(({ aggs, groupBy, initialPageSize: 5, pageSizeOptions: [5, 10, 25], }} + rowProps={() => ({ + 'data-test-subj': 'transformPivotPreviewRow', + })} sorting={sorting} /> )} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index ebb0660cac55f..b8f63ef697e78 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -37,9 +37,8 @@ import { KqlFilterBar } from '../../../../../shared_imports'; import { SwitchModal } from './switch_modal'; import { - isKibanaContextInitialized, - KibanaContext, - KibanaContextValue, + useKibanaContext, + InitializedKibanaContextValue, SavedSearchQuery, } from '../../../../lib/kibana'; @@ -75,7 +74,7 @@ const defaultSearch = '*'; const emptySearch = ''; export function getDefaultStepDefineState( - kibanaContext: KibanaContextValue + kibanaContext: InitializedKibanaContextValue ): StepDefineExposedState { return { aggList: {} as PivotAggsConfigDict, @@ -83,13 +82,9 @@ export function getDefaultStepDefineState( isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, searchString: - isKibanaContextInitialized(kibanaContext) && kibanaContext.currentSavedSearch !== undefined - ? kibanaContext.combinedQuery - : defaultSearch, + kibanaContext.currentSavedSearch !== undefined ? kibanaContext.combinedQuery : defaultSearch, searchQuery: - isKibanaContextInitialized(kibanaContext) && kibanaContext.currentSavedSearch !== undefined - ? kibanaContext.combinedQuery - : defaultSearch, + kibanaContext.currentSavedSearch !== undefined ? kibanaContext.combinedQuery : defaultSearch, sourceConfigUpdated: false, valid: false, }; @@ -196,7 +191,7 @@ interface Props { } export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange }) => { - const kibanaContext = useContext(KibanaContext); + const kibanaContext = useKibanaContext(); const defaults = { ...getDefaultStepDefineState(kibanaContext), ...overrides }; @@ -224,10 +219,6 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange // The list of selected group by fields const [groupByList, setGroupByList] = useState(defaults.groupByList); - if (!isKibanaContextInitialized(kibanaContext)) { - return null; - } - const indexPattern = kibanaContext.currentIndexPattern; const { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index bb900766483df..30c447f62c760 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, Fragment, FC } from 'react'; +import React, { Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -17,7 +17,7 @@ import { EuiText, } from '@elastic/eui'; -import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana'; +import { useKibanaContext } from '../../../../lib/kibana'; import { AggListSummary } from '../aggregation_list'; import { GroupByListSummary } from '../group_by_list'; @@ -35,11 +35,7 @@ export const StepDefineSummary: FC = ({ groupByList, aggList, }) => { - const kibanaContext = useContext(KibanaContext); - - if (!isKibanaContextInitialized(kibanaContext)) { - return null; - } + const kibanaContext = useKibanaContext(); const pivotQuery = getPivotQuery(searchQuery); let useCodeBlock = false; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts index 92e3bdded4f6a..e02f2473fc10b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/use_pivot_preview_data.ts @@ -32,7 +32,8 @@ interface EsMappingType { type: ES_FIELD_TYPES; } -type PreviewData = Array>; +export type PreviewItem = Dictionary; +type PreviewData = PreviewItem[]; interface PreviewMappings { properties: Dictionary; } diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index ef6159a1f7bb0..a01481fde343c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { metadata } from 'ui/metadata'; @@ -13,7 +13,7 @@ import { toastNotifications } from 'ui/notify'; import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; -import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana'; +import { useKibanaContext } from '../../../../lib/kibana'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; import { ToastNotificationText } from '../../../../components'; @@ -55,7 +55,7 @@ interface Props { } export const StepDetailsForm: FC = React.memo(({ overrides = {}, onChange }) => { - const kibanaContext = useContext(KibanaContext); + const kibanaContext = useKibanaContext(); const defaults = { ...getDefaultStepDetailsState(), ...overrides }; @@ -80,56 +80,47 @@ export const StepDetailsForm: FC = React.memo(({ overrides = {}, onChange useEffect(() => { // use an IIFE to avoid returning a Promise to useEffect. (async function() { - if (isKibanaContextInitialized(kibanaContext)) { - try { - setTransformIds( - (await api.getTransforms()).transforms.map( - (transform: TransformPivotConfig) => transform.id - ) - ); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', { - defaultMessage: 'An error occurred getting the existing transform IDs:', - }), - text: toMountPoint(), - }); - } + try { + setTransformIds( + (await api.getTransforms()).transforms.map( + (transform: TransformPivotConfig) => transform.id + ) + ); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', { + defaultMessage: 'An error occurred getting the existing transform IDs:', + }), + text: toMountPoint(), + }); + } - try { - setIndexNames((await api.getIndices()).map(index => index.name)); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', { - defaultMessage: 'An error occurred getting the existing index names:', - }), - text: toMountPoint(), - }); - } + try { + setIndexNames((await api.getIndices()).map(index => index.name)); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', { + defaultMessage: 'An error occurred getting the existing index names:', + }), + text: toMountPoint(), + }); + } - try { - setIndexPatternTitles(await kibanaContext.indexPatterns.getTitles()); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles', - { - defaultMessage: 'An error occurred getting the existing index pattern titles:', - } - ), - text: toMountPoint(), - }); - } + try { + setIndexPatternTitles(await kibanaContext.indexPatterns.getTitles()); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles', { + defaultMessage: 'An error occurred getting the existing index pattern titles:', + }), + text: toMountPoint(), + }); } })(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps }, [kibanaContext.initialized]); - if (!isKibanaContextInitialized(kibanaContext)) { - return null; - } - const dateFieldNames = kibanaContext.currentIndexPattern.fields .filter(f => f.type === 'date') .map(f => f.name) diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 25d3915a1eae9..109cf81da6caa 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useContext, useEffect, useRef, useState } from 'react'; +import React, { Fragment, FC, useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSteps, EuiStepStatus } from '@elastic/eui'; -import { isKibanaContextInitialized, KibanaContext } from '../../../../lib/kibana'; +import { useKibanaContext } from '../../../../lib/kibana'; import { getCreateRequestBody } from '../../../../common'; @@ -68,7 +68,7 @@ const StepDefine: FC = ({ }; export const Wizard: FC = React.memo(() => { - const kibanaContext = useContext(KibanaContext); + const kibanaContext = useKibanaContext(); // The current WIZARD_STEP const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE); @@ -108,11 +108,6 @@ export const Wizard: FC = React.memo(() => { } }, []); - if (!isKibanaContextInitialized(kibanaContext)) { - // TODO proper loading indicator - return null; - } - const indexPattern = kibanaContext.currentIndexPattern; const transformConfig = getCreateRequestBody( @@ -134,18 +129,6 @@ export const Wizard: FC = React.memo(() => { ); - // scroll to the currently selected wizard step - /* - function scrollToRef() { - if (definePivotRef !== null && definePivotRef.current !== null) { - // TODO Fix types - const dummy = definePivotRef as any; - const headerOffset = 70; - window.scrollTo(0, dummy.current.offsetTop - headerOffset); - } - } - */ - const stepsConfig = [ { title: i18n.translate('xpack.transform.transformsWizard.stepDefineTitle', { @@ -171,7 +154,6 @@ export const Wizard: FC = React.memo(() => { { setCurrentStep(WIZARD_STEPS.DEFINE); - // scrollToRef(); }} next={() => setCurrentStep(WIZARD_STEPS.CREATE)} nextActive={stepDetailsState.valid} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index 2214d1f5adfff..f63f3b6d6e7be 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -26,7 +26,7 @@ import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/cons import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; import { documentationLinksService } from '../../services/documentation'; import { PrivilegesWrapper } from '../../lib/authorization'; -import { KibanaProvider } from '../../lib/kibana'; +import { KibanaProvider, RenderOnlyWithInitializedKibanaContext } from '../../lib/kibana'; import { Wizard } from './components/wizard'; @@ -82,7 +82,9 @@ export const CreateTransformSection: FC = ({ match }) => { - + + + diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap index 40ad836ad9969..1f134cd39948b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row.test.tsx.snap @@ -90,7 +90,8 @@ exports[`Transform: Transform List Minimal initialization 1`] = ] } />, - "id": "transform-details", + "data-test-subj": "transformDetailsTab", + "id": "transform-details-tab-fq_date_histogram_1m_1441", "name": "Transform details", } } @@ -188,7 +189,8 @@ exports[`Transform: Transform List Minimal initialization 1`] = ] } />, - "id": "transform-details", + "data-test-subj": "transformDetailsTab", + "id": "transform-details-tab-fq_date_histogram_1m_1441", "name": "Transform details", }, Object { @@ -229,14 +231,16 @@ exports[`Transform: Transform List Minimal initialization 1`] = } } />, - "id": "transform-json", + "data-test-subj": "transformJsonTab", + "id": "transform-json-tab-fq_date_histogram_1m_1441", "name": "JSON", }, Object { "content": , - "id": "transform-messages", + "data-test-subj": "transformMessagesTab", + "id": "transform-messages-tab-fq_date_histogram_1m_1441", "name": "Messages", }, Object { @@ -277,7 +281,8 @@ exports[`Transform: Transform List Minimal initialization 1`] = } } />, - "id": "transform-preview", + "data-test-subj": "transformPreviewTab", + "id": "transform-preview-tab-fq_date_histogram_1m_1441", "name": "Preview", }, ] diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap index b55a1c410d687..39964399f66db 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_details_pane.test.tsx.snap @@ -1,40 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Transform: Job List Expanded Row Minimal initialization 1`] = ` - - + + - -
    + +
    + + - - - + +
    `; exports[`Transform: Job List Expanded Row
    Minimal initialization 1`] = ` diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap index 0d4a80a94ee51..dea6f57bcaab0 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap @@ -1,22 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Transform: Transform List Expanded Row Minimal initialization 1`] = ` - - - - + + + + Minimal \\"version\\": \\"8.0.0\\", \\"create_time\\": 1564388146667 }" - /> - - -   - - + /> + + +   + + + `; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx index 19ab74cc9ed85..050dedbc8e0b4 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx @@ -88,14 +88,14 @@ export const getColumns = ( const columns: [ ExpanderColumnType, - FieldDataColumnType, - FieldDataColumnType, - FieldDataColumnType, - FieldDataColumnType, - ComputedColumnType, - ComputedColumnType, - ComputedColumnType, - ActionsColumnType + FieldDataColumnType, + FieldDataColumnType, + FieldDataColumnType, + FieldDataColumnType, + ComputedColumnType, + ComputedColumnType, + ComputedColumnType, + ActionsColumnType ] = [ { align: RIGHT_ALIGNMENT, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index 687bb0df3f577..c02b7e9ce5b1b 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -121,7 +121,8 @@ export const ExpandedRow: FC = ({ item }) => { const tabs = [ { - id: 'transform-details', + id: `transform-details-tab-${item.id}`, + 'data-test-subj': 'transformDetailsTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformSettingsLabel', { @@ -131,12 +132,14 @@ export const ExpandedRow: FC = ({ item }) => { content: , }, { - id: 'transform-json', + id: `transform-json-tab-${item.id}`, + 'data-test-subj': 'transformJsonTab', name: 'JSON', content: , }, { - id: 'transform-messages', + id: `transform-messages-tab-${item.id}`, + 'data-test-subj': 'transformMessagesTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel', { @@ -146,7 +149,8 @@ export const ExpandedRow: FC = ({ item }) => { content: , }, { - id: 'transform-preview', + id: `transform-preview-tab-${item.id}`, + 'data-test-subj': 'transformPreviewTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel', { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx index cae95286c464d..527033c46b469 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx @@ -50,27 +50,29 @@ interface ExpandedRowDetailsPaneProps { export const ExpandedRowDetailsPane: FC = ({ sections }) => { return ( - - - {sections - .filter(s => s.position === 'left') - .map(s => ( - - -
    - - ))} - - - {sections - .filter(s => s.position === 'right') - .map(s => ( - - -
    - - ))} - - +
    + + + {sections + .filter(s => s.position === 'left') + .map(s => ( + + +
    + + ))} + + + {sections + .filter(s => s.position === 'right') + .map(s => ( + + +
    + + ))} + + +
    ); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx index ac7fdcb129531..6792f4b80f665 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx @@ -20,18 +20,20 @@ interface Props { export const ExpandedRowJsonPane: FC = ({ json }) => { return ( - - - - - -   - +
    + + + + + +   + +
    ); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx index 3f0a9a410f17e..1aeb93c162847 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; // @ts-ignore @@ -143,7 +143,7 @@ export const ExpandedRowMessagesPane: React.FC = ({ transformId }) => { }; return ( - +
    = ({ transformId }) => { pagination={pagination} onChange={onChange} /> - +
    ); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 68f2964d41e57..5a5e8308b8d57 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -14,12 +14,13 @@ import { useApi } from '../../../../hooks/use_api'; import { getFlattenedFields, useRefreshTransformList, + EsDoc, PreviewRequestBody, TransformPivotConfig, } from '../../../../common'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; -import { TransformTable } from './transform_table'; +import { transformTableFactory } from './transform_table'; interface Props { transformConfig: TransformPivotConfig; @@ -45,12 +46,14 @@ function getDataFromTransform( transformConfig: TransformPivotConfig ): { previewRequest: PreviewRequestBody; groupByArr: string[] | [] } { const index = transformConfig.source.index; + const query = transformConfig.source.query; const pivot = transformConfig.pivot; const groupByArr = []; const previewRequest: PreviewRequestBody = { source: { index, + query, }, pivot, }; @@ -67,8 +70,8 @@ function getDataFromTransform( } export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { - const [previewData, setPreviewData] = useState([]); - const [columns, setColumns] = useState([]); + const [previewData, setPreviewData] = useState([]); + const [columns, setColumns] = useState> | []>([]); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [sortField, setSortField] = useState(''); @@ -97,8 +100,8 @@ export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { const columnKeys = getFlattenedFields(resp.preview[0]); columnKeys.sort(sortColumns(groupByArr)); - const tableColumns: FieldDataColumnType[] = columnKeys.map(k => { - const column: FieldDataColumnType = { + const tableColumns: Array> = columnKeys.map(k => { + const column: FieldDataColumnType = { field: k, name: k, sortable: true, @@ -191,17 +194,27 @@ export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { setSortDirection(direction); }; + const transformTableLoading = previewData.length === 0 && isLoading === true; + const dataTestSubj = `transformPreviewTabContent${!transformTableLoading ? ' loaded' : ''}`; + + const TransformTable = transformTableFactory(); + return ( - +
    + ({ + 'data-test-subj': 'transformPreviewTabContentRow', + })} + sorting={sorting} + error={errorMessage} + /> +
    ); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index 02abadb85fbd0..e1a65f631df3c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -42,7 +42,7 @@ import { StopAction } from './action_stop'; import { ItemIdToExpandedRowMap, Query, Clause } from './common'; import { getColumns } from './columns'; import { ExpandedRow } from './expanded_row'; -import { ProgressBar, TransformTable } from './transform_table'; +import { ProgressBar, transformTableFactory } from './transform_table'; function getItemIdToExpandedRowMap( itemIds: TransformId[], @@ -374,6 +374,8 @@ export const TransformList: FC = ({ onSelectionChange: (selected: TransformListRow[]) => setTransformSelection(selected), }; + const TransformTable = transformTableFactory(); + return (
    diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx index cd6f6654a8e9e..8c7920c124bef 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx @@ -11,7 +11,7 @@ import React, { Fragment } from 'react'; import { EuiProgress } from '@elastic/eui'; -import { MlInMemoryTableBasic } from '../../../../../shared_imports'; +import { mlInMemoryTableBasicFactory } from '../../../../../shared_imports'; // The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement // of the table and doesn't play well with auto-refreshing. That's why we're displaying @@ -73,32 +73,35 @@ const getInitialSorting = (columns: any, sorting: any) => { }; }; -export class TransformTable extends MlInMemoryTableBasic { - static getDerivedStateFromProps(nextProps: any, prevState: any) { - const derivedState = { - ...prevState.prevProps, - pageIndex: nextProps.pagination.initialPageIndex, - pageSize: nextProps.pagination.initialPageSize, - }; +export function transformTableFactory() { + const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + return class TransformTable extends MlInMemoryTableBasic { + static getDerivedStateFromProps(nextProps: any, prevState: any) { + const derivedState = { + ...prevState.prevProps, + pageIndex: nextProps.pagination.initialPageIndex, + pageSize: nextProps.pagination.initialPageSize, + }; - if (nextProps.items !== prevState.prevProps.items) { - Object.assign(derivedState, { - prevProps: { - items: nextProps.items, - }, - }); - } + if (nextProps.items !== prevState.prevProps.items) { + Object.assign(derivedState, { + prevProps: { + items: nextProps.items, + }, + }); + } - const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); - if ( - sortName !== prevState.prevProps.sortName || - sortDirection !== prevState.prevProps.sortDirection - ) { - Object.assign(derivedState, { - sortName, - sortDirection, - }); + const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); + if ( + sortName !== prevState.prevProps.sortName || + sortDirection !== prevState.prevProps.sortDirection + ) { + Object.assign(derivedState, { + sortName, + sortDirection, + }); + } + return derivedState; } - return derivedState; - } + }; } diff --git a/x-pack/legacy/plugins/transform/public/shared_imports.ts b/x-pack/legacy/plugins/transform/public/shared_imports.ts index 09af7b3c6f844..fe9fe8b8e695b 100644 --- a/x-pack/legacy/plugins/transform/public/shared_imports.ts +++ b/x-pack/legacy/plugins/transform/public/shared_imports.ts @@ -25,7 +25,7 @@ export { ExpanderColumnType, FieldDataColumnType, ColumnType, - MlInMemoryTableBasic, + mlInMemoryTableBasicFactory, OnTableChangeArg, SortingPropType, SortDirection, diff --git a/x-pack/test/functional/apps/transform/creation.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts similarity index 91% rename from x-pack/test/functional/apps/transform/creation.ts rename to x-pack/test/functional/apps/transform/creation_index_pattern.ts index 3ab17c0d90a83..3dbf61221abf9 100644 --- a/x-pack/test/functional/apps/transform/creation.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -17,7 +17,7 @@ export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - describe('creation', function() { + describe('creation_index_pattern', function() { this.tags(['smoke']); before(async () => { await esArchiver.load('ml/ecommerce'); @@ -56,11 +56,19 @@ export default function({ getService }: FtrProviderContext) { return `dest_${this.transformId}`; }, expected: { + pivotPreview: { + column: 0, + values: [`Men's Accessories`], + }, row: { status: 'stopped', mode: 'batch', progress: '100', }, + sourcePreview: { + columns: 6, + rows: 5, + }, }, }, ]; @@ -96,6 +104,13 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertSourceIndexPreviewLoaded(); }); + it('shows the source index preview', async () => { + await transform.wizard.assertSourceIndexPreview( + testData.expected.sourcePreview.columns, + testData.expected.sourcePreview.rows + ); + }); + it('displays an empty pivot preview', async () => { await transform.wizard.assertPivotPreviewEmpty(); }); @@ -140,6 +155,13 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertPivotPreviewLoaded(); }); + it('shows the pivot preview', async () => { + await transform.wizard.assertPivotPreviewColumnValues( + testData.expected.pivotPreview.column, + testData.expected.pivotPreview.values + ); + }); + it('loads the details step', async () => { await transform.wizard.advanceToDetailsStep(); }); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts new file mode 100644 index 0000000000000..8a69700bee578 --- /dev/null +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +interface GroupByEntry { + identifier: string; + label: string; + intervalLabel?: string; +} + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('creation_saved_search', function() { + this.tags(['smoke']); + before(async () => { + await esArchiver.load('ml/farequote'); + }); + + // after(async () => { + // await esArchiver.unload('ml/farequote'); + // await transform.api.cleanTransformIndices(); + // }); + + const testDataList = [ + { + suiteTitle: 'batch transform with terms groups and avg agg with saved search filter', + source: 'farequote_filter', + groupByEntries: [ + { + identifier: 'terms(airline)', + label: 'airline', + } as GroupByEntry, + ], + aggregationEntries: [ + { + identifier: 'avg(responsetime)', + label: 'responsetime.avg', + }, + ], + transformId: `fq_1_${Date.now()}`, + transformDescription: + 'farequote batch transform with groups terms(airline) and aggregation avg(responsetime.avg) with saved search filter', + get destinationIndex(): string { + return `dest_${this.transformId}`; + }, + expected: { + pivotPreview: { + column: 0, + values: ['ASA'], + }, + row: { + status: 'stopped', + mode: 'batch', + progress: '100', + }, + sourceIndex: 'farequote', + sourcePreview: { + column: 3, + values: ['ASA'], + }, + }, + }, + ]; + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + after(async () => { + await transform.api.deleteIndices(testData.destinationIndex); + }); + + it('loads the home page', async () => { + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + }); + + it('displays the stats bar', async () => { + await transform.management.assertTransformStatsBarExists(); + }); + + it('loads the source selection modal', async () => { + await transform.management.startTransformCreation(); + }); + + it('selects the source data', async () => { + await transform.sourceSelection.selectSource(testData.source); + }); + + it('displays the define pivot step', async () => { + await transform.wizard.assertDefineStepActive(); + }); + + it('loads the source index preview', async () => { + await transform.wizard.assertSourceIndexPreviewLoaded(); + }); + + it('shows the filtered source index preview', async () => { + await transform.wizard.assertSourceIndexPreviewColumnValues( + testData.expected.sourcePreview.column, + testData.expected.sourcePreview.values + ); + }); + + it('displays an empty pivot preview', async () => { + await transform.wizard.assertPivotPreviewEmpty(); + }); + + it('hides the query input', async () => { + await transform.wizard.assertQueryInputMissing(); + }); + + it('hides the advanced query editor switch', async () => { + await transform.wizard.assertAdvancedQueryEditorSwitchMissing(); + }); + + it('adds the group by entries', async () => { + for (const [index, entry] of testData.groupByEntries.entries()) { + await transform.wizard.assertGroupByInputExists(); + await transform.wizard.assertGroupByInputValue([]); + await transform.wizard.addGroupByEntry( + index, + entry.identifier, + entry.label, + entry.intervalLabel + ); + } + }); + + it('adds the aggregation entries', async () => { + for (const [index, agg] of testData.aggregationEntries.entries()) { + await transform.wizard.assertAggregationInputExists(); + await transform.wizard.assertAggregationInputValue([]); + await transform.wizard.addAggregationEntry(index, agg.identifier, agg.label); + } + }); + + it('displays the advanced pivot editor switch', async () => { + await transform.wizard.assertAdvancedPivotEditorSwitchExists(); + await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); + }); + + it('loads the pivot preview', async () => { + await transform.wizard.assertPivotPreviewLoaded(); + }); + + it('shows the pivot preview', async () => { + await transform.wizard.assertPivotPreviewColumnValues( + testData.expected.pivotPreview.column, + testData.expected.pivotPreview.values + ); + }); + + it('loads the details step', async () => { + await transform.wizard.advanceToDetailsStep(); + }); + + it('inputs the transform id', async () => { + await transform.wizard.assertTransformIdInputExists(); + await transform.wizard.assertTransformIdValue(''); + await transform.wizard.setTransformId(testData.transformId); + }); + + it('inputs the transform description', async () => { + await transform.wizard.assertTransformDescriptionInputExists(); + await transform.wizard.assertTransformDescriptionValue(''); + await transform.wizard.setTransformDescription(testData.transformDescription); + }); + + it('inputs the destination index', async () => { + await transform.wizard.assertDestinationIndexInputExists(); + await transform.wizard.assertDestinationIndexValue(''); + await transform.wizard.setDestinationIndex(testData.destinationIndex); + }); + + it('displays the create index pattern switch', async () => { + await transform.wizard.assertCreateIndexPatternSwitchExists(); + await transform.wizard.assertCreateIndexPatternSwitchCheckState(true); + }); + + it('displays the continuous mode switch', async () => { + await transform.wizard.assertContinuousModeSwitchExists(); + await transform.wizard.assertContinuousModeSwitchCheckState(false); + }); + + it('loads the create step', async () => { + await transform.wizard.advanceToCreateStep(); + }); + + it('displays the create and start button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + }); + + it('displays the create button', async () => { + await transform.wizard.assertCreateButtonExists(); + }); + + it('displays the copy to clipboard button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + }); + + it('creates the transform', async () => { + await transform.wizard.createTransform(); + }); + + it('starts the transform and finishes processing', async () => { + await transform.wizard.startTransform(); + await transform.wizard.waitForProgressBarComplete(); + }); + + it('returns to the management page', async () => { + await transform.wizard.returnToManagement(); + }); + + it('displays the transforms table', async () => { + await transform.management.assertTransformsTableExists(); + }); + + it('displays the created transform in the transform list', async () => { + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(testData.transformId); + const rows = await transform.table.parseTransformTable(); + expect(rows.filter(row => row.id === testData.transformId)).to.have.length(1); + }); + + it('job creation displays details for the created job in the job list', async () => { + await transform.table.assertTransformRowFields(testData.transformId, { + id: testData.transformId, + description: testData.transformDescription, + sourceIndex: testData.expected.sourceIndex, + destinationIndex: testData.destinationIndex, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + }); + + it('expands the transform management table row and walks through available tabs', async () => { + await transform.table.assertTransformExpandedRow(); + }); + + it('displays the transform preview in the expanded row', async () => { + await transform.table.assertTransformsExpandedRowPreviewColumnValues( + testData.expected.pivotPreview.column, + testData.expected.pivotPreview.values + ); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index adee997905a31..0a33ce0ebf08a 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -9,6 +9,7 @@ export default function({ loadTestFile }: FtrProviderContext) { describe('transform', function() { this.tags(['ciGroup9', 'transform']); - loadTestFile(require.resolve('./creation')); + loadTestFile(require.resolve('./creation_index_pattern')); + loadTestFile(require.resolve('./creation_saved_search')); }); } diff --git a/x-pack/test/functional/services/transform_ui/transform_table.ts b/x-pack/test/functional/services/transform_ui/transform_table.ts index b9eff5e2b2435..ebd7fe527b45f 100644 --- a/x-pack/test/functional/services/transform_ui/transform_table.ts +++ b/x-pack/test/functional/services/transform_ui/transform_table.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformTableProvider({ getService }: FtrProviderContext) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return new (class TransformTable { @@ -60,6 +61,51 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { return rows; } + async parseEuiInMemoryTable(tableSubj: string) { + const table = await testSubjects.find(`~${tableSubj}`); + const $ = await table.parseDomContent(); + const rows = []; + + // For each row, get the content of each cell and + // add its values as an array to each row. + for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) { + rows.push( + $(tr) + .find('.euiTableCellContent') + .toArray() + .map(cell => + $(cell) + .text() + .trim() + ) + ); + } + + return rows; + } + + async assertEuiInMemoryTableColumnValues( + tableSubj: string, + column: number, + expectedColumnValues: string[] + ) { + await retry.tryForTime(2000, async () => { + // get a 2D array of rows and cell values + const rows = await this.parseEuiInMemoryTable(tableSubj); + + // reduce the rows data to an array of unique values in the specified column + const uniqueColumnValues = rows + .map(row => row[column]) + .flat() + .filter((v, i, a) => a.indexOf(v) === i); + + uniqueColumnValues.sort(); + + // check if the returned unique value matches the supplied filter value + expect(uniqueColumnValues).to.eql(expectedColumnValues); + }); + } + public async refreshTransformList() { await testSubjects.click('transformRefreshTransformListButton'); await this.waitForTransformsToLoad(); @@ -83,5 +129,36 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { const transformRow = rows.filter(row => row.id === transformId)[0]; expect(transformRow).to.eql(expectedRow); } + + public async assertTransformExpandedRow() { + await testSubjects.click('transformListRowDetailsToggle'); + + // The expanded row should show the details tab content by default + await testSubjects.existOrFail('transformDetailsTab'); + await testSubjects.existOrFail('~transformDetailsTabContent'); + + // Walk through the rest of the tabs and check if the corresponding content shows up + await testSubjects.existOrFail('transformJsonTab'); + await testSubjects.click('transformJsonTab'); + await testSubjects.existOrFail('~transformJsonTabContent'); + + await testSubjects.existOrFail('transformMessagesTab'); + await testSubjects.click('transformMessagesTab'); + await testSubjects.existOrFail('~transformMessagesTabContent'); + + await testSubjects.existOrFail('transformPreviewTab'); + await testSubjects.click('transformPreviewTab'); + await testSubjects.existOrFail('~transformPreviewTabContent'); + } + + public async waitForTransformsExpandedRowPreviewTabToLoad() { + await testSubjects.existOrFail('~transformPreviewTabContent', { timeout: 60 * 1000 }); + await testSubjects.existOrFail('transformPreviewTabContent loaded', { timeout: 30 * 1000 }); + } + + async assertTransformsExpandedRowPreviewColumnValues(column: number, values: string[]) { + await this.waitForTransformsExpandedRowPreviewTabToLoad(); + await this.assertEuiInMemoryTableColumnValues('transformPreviewTabContent', column, values); + } })(); } diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index c80aa62cd4912..db7cdd148fd99 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -75,6 +75,81 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, + async parseEuiInMemoryTable(tableSubj: string) { + const table = await testSubjects.find(`~${tableSubj}`); + const $ = await table.parseDomContent(); + const rows = []; + + // For each row, get the content of each cell and + // add its values as an array to each row. + for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) { + rows.push( + $(tr) + .find('.euiTableCellContent') + .toArray() + .map(cell => + $(cell) + .text() + .trim() + ) + ); + } + + return rows; + }, + + async assertEuiInMemoryTableColumnValues( + tableSubj: string, + column: number, + expectedColumnValues: string[] + ) { + await retry.tryForTime(2000, async () => { + // get a 2D array of rows and cell values + const rows = await this.parseEuiInMemoryTable(tableSubj); + + // reduce the rows data to an array of unique values in the specified column + const uniqueColumnValues = rows + .map(row => row[column]) + .flat() + .filter((v, i, a) => a.indexOf(v) === i); + + uniqueColumnValues.sort(); + + // check if the returned unique value matches the supplied filter value + expect(uniqueColumnValues).to.eql( + expectedColumnValues, + `Unique EuiInMemoryTable column values should be '${expectedColumnValues.join()}' (got ${uniqueColumnValues.join()})` + ); + }); + }, + + async assertSourceIndexPreview(columns: number, rows: number) { + await retry.tryForTime(2000, async () => { + // get a 2D array of rows and cell values + const rowsData = await this.parseEuiInMemoryTable('transformSourceIndexPreview'); + + expect(rowsData).to.length( + rows, + `EuiInMemoryTable rows should be ${rows} (got ${rowsData.length})` + ); + + rowsData.map((r, i) => + expect(r).to.length( + columns, + `EuiInMemoryTable row #${i + 1} column count should be ${columns} (got ${r.length})` + ) + ); + }); + }, + + async assertSourceIndexPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiInMemoryTableColumnValues('transformSourceIndexPreview', column, values); + }, + + async assertPivotPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiInMemoryTableColumnValues('transformPivotPreview', column, values); + }, + async assertPivotPreviewLoaded() { await this.assertPivotPreviewExists('loaded'); }, @@ -87,6 +162,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('tarnsformQueryInput'); }, + async assertQueryInputMissing() { + await testSubjects.missingOrFail('tarnsformQueryInput'); + }, + async assertQueryValue(expectedQuery: string) { const actualQuery = await testSubjects.getVisibleText('tarnsformQueryInput'); expect(actualQuery).to.eql( @@ -99,6 +178,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(`transformAdvancedQueryEditorSwitch`, { allowHidden: true }); }, + async assertAdvancedQueryEditorSwitchMissing() { + await testSubjects.missingOrFail(`transformAdvancedQueryEditorSwitch`); + }, + async assertAdvancedQueryEditorSwitchCheckState(expectedCheckState: boolean) { const actualCheckState = (await testSubjects.getAttribute('transformAdvancedQueryEditorSwitch', 'aria-checked')) === From f32c1e439eb7eca472a7eb35fc302bdc7bf8b48e Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Mon, 25 Nov 2019 10:07:00 +0100 Subject: [PATCH 041/128] [SIEM] Add hosts and network anomalies histogram (#50295) --- package.json | 4 +- .../components/anomalies_over_time/index.tsx | 30 ++ .../anomalies_over_time/translation.ts | 24 ++ .../anomalies_over_time.gql_query.ts | 37 +++ .../anomalies/anomalies_over_time/index.tsx | 86 +++++ .../anomalies/anomalies_over_time/types.ts | 32 ++ .../anomalies_query_tab_body/index.tsx | 93 ++++++ .../anomalies_query_tab_body/types.ts | 34 ++ .../anomalies_query_tab_body/utils.ts | 68 ++++ .../siem/public/graphql/introspection.json | 298 ++++++++++++------ .../plugins/siem/public/graphql/types.ts | 97 +++++- .../pages/hosts/details/details_tabs.tsx | 7 +- .../siem/public/pages/hosts/hosts_tabs.tsx | 7 +- .../navigation/anomalies_query_tab_body.tsx | 29 -- .../public/pages/hosts/navigation/index.ts | 1 - .../public/pages/hosts/navigation/types.ts | 31 +- .../public/pages/network/ip_details/index.tsx | 28 +- .../navigation/anomalies_query_tab_body.tsx | 27 -- .../navigation/countries_query_tab_body.tsx | 12 +- .../network/navigation/dns_query_tab_body.tsx | 26 +- .../navigation/http_query_tab_body.tsx | 12 +- .../public/pages/network/navigation/index.ts | 1 - .../network/navigation/ips_query_tab_body.tsx | 12 +- .../network/navigation/network_routes.tsx | 52 ++- .../network/navigation/tls_query_tab_body.tsx | 12 +- .../public/pages/network/navigation/types.ts | 38 +-- .../siem/server/graphql/anomalies/index.ts | 8 + .../server/graphql/anomalies/resolvers.ts | 38 +++ .../server/graphql/anomalies/schema.gql.ts | 23 ++ .../plugins/siem/server/graphql/index.ts | 2 + .../plugins/siem/server/graphql/types.ts | 185 +++++++---- .../legacy/plugins/siem/server/init_server.ts | 2 + .../lib/anomalies/elasticsearch_adapter.ts | 64 ++++ .../siem/server/lib/anomalies/index.ts | 21 ++ .../query.anomalies_over_time.dsl.ts | 75 +++++ .../siem/server/lib/anomalies/types.ts | 42 +++ .../query.authentications_over_time.dsl.ts | 3 +- .../plugins/siem/server/lib/compose/kibana.ts | 3 + .../lib/events/query.events_over_time.dsl.ts | 3 +- .../legacy/plugins/siem/server/lib/types.ts | 2 + .../calculate_timeseries_interval.ts | 3 +- yarn.lock | 13 +- 42 files changed, 1234 insertions(+), 351 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/anomalies_over_time/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/anomalies_over_time/translation.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/anomalies_over_time.gql_query.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts delete mode 100644 x-pack/legacy/plugins/siem/public/pages/hosts/navigation/anomalies_query_tab_body.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/pages/network/navigation/anomalies_query_tab_body.tsx create mode 100644 x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts diff --git a/package.json b/package.json index 04415b481d5dd..a1873134e3cb8 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ "**/typescript": "3.7.2", "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", - "**/image-diff/gm/debug": "^2.6.9" + "**/image-diff/gm/debug": "^2.6.9", + "**/deepmerge": "^4.2.2" }, "workspaces": { "packages": [ @@ -155,6 +156,7 @@ "custom-event-polyfill": "^0.3.0", "d3": "3.5.17", "d3-cloud": "1.2.5", + "deepmerge": "^4.2.2", "del": "^5.1.0", "elasticsearch": "^16.5.0", "elasticsearch-browser": "^16.5.0", diff --git a/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/index.tsx new file mode 100644 index 0000000000000..2337f2cd7512a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { MatrixHistogramBasicProps } from '../matrix_histogram/types'; +import { MatrixOverTimeHistogramData } from '../../graphql/types'; +import { MatrixHistogram } from '../matrix_histogram'; +import * as i18n from './translation'; + +export const AnomaliesOverTimeHistogram = ( + props: MatrixHistogramBasicProps +) => { + const dataKey = 'anomaliesOverTime'; + const { totalCount } = props; + const subtitle = `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(totalCount)}`; + const { ...matrixOverTimeProps } = props; + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/translation.ts b/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/translation.ts new file mode 100644 index 0000000000000..f28a7176fd09d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/anomalies_over_time/translation.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ANOMALIES_COUNT_FREQUENCY_BY_ACTION = i18n.translate( + 'xpack.siem.anomaliesOverTime.anomaliesCountFrequencyByJobTile', + { + defaultMessage: 'Anomalies count by job', + } +); + +export const SHOWING = i18n.translate('xpack.siem.anomaliesOverTime.showing', { + defaultMessage: 'Showing', +}); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.anomaliesOverTime.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {anomaly} other {anomalies}}`, + }); diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/anomalies_over_time.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/anomalies_over_time.gql_query.ts new file mode 100644 index 0000000000000..498cdaec131e8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/anomalies_over_time.gql_query.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const AnomaliesOverTimeGqlQuery = gql` + query GetAnomaliesOverTimeQuery( + $sourceId: ID! + $timerange: TimerangeInput! + $defaultIndex: [String!]! + $filterQuery: String + $inspect: Boolean! + ) { + source(id: $sourceId) { + id + AnomaliesOverTime( + timerange: $timerange + filterQuery: $filterQuery + defaultIndex: $defaultIndex + ) { + anomaliesOverTime { + x + y + g + } + totalCount + inspect @include(if: $inspect) { + dsl + response + } + } + } + } +`; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/index.tsx new file mode 100644 index 0000000000000..0d1ffba1ecd82 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/index.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; + +import { State, inputsSelectors } from '../../../store'; +import { getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplate } from '../../query_template'; + +import { AnomaliesOverTimeGqlQuery } from './anomalies_over_time.gql_query'; +import { GetAnomaliesOverTimeQuery } from '../../../graphql/types'; +import { AnomaliesOverTimeProps, OwnProps } from './types'; + +const ID = 'anomaliesOverTimeQuery'; + +class AnomaliesOverTimeComponentQuery extends QueryTemplate< + AnomaliesOverTimeProps, + GetAnomaliesOverTimeQuery.Query, + GetAnomaliesOverTimeQuery.Variables +> { + public render() { + const { + children, + endDate, + filterQuery, + id = ID, + isInspected, + sourceId, + startDate, + } = this.props; + + return ( + + query={AnomaliesOverTimeGqlQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={{ + filterQuery, + sourceId, + timerange: { + interval: 'day', + from: startDate!, + to: endDate!, + }, + defaultIndex: ['.ml-anomalies-*'], + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const source = getOr({}, `source.AnomaliesOverTime`, data); + const anomaliesOverTime = getOr([], `anomaliesOverTime`, source); + const totalCount = getOr(-1, 'totalCount', source); + return children!({ + endDate: endDate!, + anomaliesOverTime, + id, + inspect: getOr(null, 'inspect', source), + loading, + refetch, + startDate: startDate!, + totalCount, + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const AnomaliesOverTimeQuery = connect(makeMapStateToProps)(AnomaliesOverTimeComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/types.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/types.ts new file mode 100644 index 0000000000000..e6ece4a46e44f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_over_time/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { QueryTemplateProps } from '../../query_template'; +import { inputsModel, hostsModel, networkModel } from '../../../store'; +import { MatrixOverTimeHistogramData } from '../../../graphql/types'; + +export interface AnomaliesArgs { + endDate: number; + anomaliesOverTime: MatrixOverTimeHistogramData[]; + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + refetch: inputsModel.Refetch; + startDate: number; + totalCount: number; +} + +export interface OwnProps extends Omit { + filterQuery?: string; + children?: (args: AnomaliesArgs) => React.ReactNode; + type: hostsModel.HostsType | networkModel.NetworkType; +} + +export interface AnomaliesOverTimeComponentReduxProps { + isInspected: boolean; +} + +export type AnomaliesOverTimeProps = OwnProps & AnomaliesOverTimeComponentReduxProps; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx new file mode 100644 index 0000000000000..917f4dbcc211b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { AnomaliesQueryTabBodyProps } from './types'; +import { manageQuery } from '../../../components/page/manage_query'; +import { AnomaliesOverTimeHistogram } from '../../../components/anomalies_over_time'; +import { AnomaliesOverTimeQuery } from '../anomalies_over_time'; +import { getAnomaliesFilterQuery } from './utils'; +import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; + +const AnomaliesOverTimeManage = manageQuery(AnomaliesOverTimeHistogram); + +export const AnomaliesQueryTabBody = ({ + endDate, + skip, + startDate, + type, + narrowDateRange, + filterQuery, + anomaliesFilterQuery, + setQuery, + hideHistogramIfEmpty, + updateDateRange = () => {}, + AnomaliesTableComponent, + flowTarget, + ip, +}: AnomaliesQueryTabBodyProps) => { + const [siemJobsLoading, siemJobs] = useSiemJobs(true); + const [anomalyScore] = useKibanaUiSetting(DEFAULT_ANOMALY_SCORE); + + const mergedFilterQuery = getAnomaliesFilterQuery( + filterQuery, + anomaliesFilterQuery, + siemJobs, + anomalyScore, + flowTarget, + ip + ); + + return ( + <> + + {({ anomaliesOverTime, loading, id, inspect, refetch, totalCount }) => { + if (hideHistogramIfEmpty && !anomaliesOverTime.length) { + return
    ; + } + + return ( + <> + + + + ); + }} + + + + ); +}; + +AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts new file mode 100644 index 0000000000000..0aef02ddd929a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESTermQuery } from '../../../../common/typed_json'; +import { NarrowDateRange } from '../../../components/ml/types'; +import { UpdateDateRange } from '../../../components/charts/common'; +import { SetQuery } from '../../../pages/hosts/navigation/types'; +import { FlowTarget } from '../../../graphql/types'; +import { HostsType } from '../../../store/hosts/model'; +import { NetworkType } from '../../../store/network/model'; +import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; + +interface QueryTabBodyProps { + type: HostsType | NetworkType; + filterQuery?: string | ESTermQuery; +} + +export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { + startDate: number; + endDate: number; + skip: boolean; + setQuery: SetQuery; + narrowDateRange: NarrowDateRange; + updateDateRange?: UpdateDateRange; + anomaliesFilterQuery?: object; + hideHistogramIfEmpty?: boolean; + ip?: string; + flowTarget?: FlowTarget; + AnomaliesTableComponent: typeof AnomaliesHostTable | typeof AnomaliesNetworkTable; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts new file mode 100644 index 0000000000000..9609619916ab1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import deepmerge from 'deepmerge'; +import { createFilter } from '../../helpers'; +import { ESTermQuery } from '../../../../common/typed_json'; +import { SiemJob } from '../../../components/ml_popover/types'; +import { FlowTarget } from '../../../graphql/types'; + +export const getAnomaliesFilterQuery = ( + filterQuery: string | ESTermQuery | undefined, + anomaliesFilterQuery: object = {}, + siemJobs: SiemJob[] = [], + anomalyScore: number, + flowTarget?: FlowTarget, + ip?: string +): string => { + const siemJobIds = siemJobs + .filter(job => job.isInstalled) + .map(job => job.id) + .map(jobId => ({ + match_phrase: { + job_id: jobId, + }, + })); + + const filterQueryString = createFilter(filterQuery); + const filterQueryObject = filterQueryString ? JSON.parse(filterQueryString) : {}; + const mergedFilterQuery = deepmerge.all([ + filterQueryObject, + anomaliesFilterQuery, + { + bool: { + filter: [ + { + bool: { + should: siemJobIds, + minimum_should_match: 1, + }, + }, + { + match_phrase: { + result_type: 'record', + }, + }, + flowTarget && + ip && { + match_phrase: { + [`${flowTarget}.ip`]: ip, + }, + }, + { + range: { + record_score: { + gte: anomalyScore, + }, + }, + }, + ], + }, + }, + ]); + + return JSON.stringify(mergedFilterQuery); +}; diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index a93168c835293..7c173a9a90626 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -666,6 +666,53 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "AnomaliesOverTime", + "description": "", + "args": [ + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "filterQuery", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "defaultIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "AnomaliesOverTimeData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "Authentications", "description": "Gets Authentication success and failures based on a timerange", @@ -2491,6 +2538,159 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "AnomaliesOverTimeData", + "description": "", + "fields": [ + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anomaliesOverTime", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Inspect", + "description": "", + "fields": [ + { + "name": "dsl", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "response", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "description": "", + "fields": [ + { + "name": "x", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "y", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "g", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "PaginationInputPaginated", @@ -3200,57 +3400,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "Inspect", - "description": "", - "fields": [ - { - "name": "dsl", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "response", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "AuthenticationsOverTimeData", @@ -3306,53 +3455,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "description": "", - "fields": [ - { - "name": "x", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "y", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "g", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "INPUT_OBJECT", "name": "PaginationInput", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index ad05e42bcd859..1464b55648035 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -456,6 +456,8 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; + + AnomaliesOverTime: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; @@ -556,6 +558,28 @@ export interface IndexField { format?: Maybe; } +export interface AnomaliesOverTimeData { + inspect?: Maybe; + + anomaliesOverTime: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface Inspect { + dsl: string[]; + + response: string[]; +} + +export interface MatrixOverTimeHistogramData { + x: number; + + y: number; + + g: string; +} + export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -690,12 +714,6 @@ export interface PageInfoPaginated { showMorePagesIndicator: boolean; } -export interface Inspect { - dsl: string[]; - - response: string[]; -} - export interface AuthenticationsOverTimeData { inspect?: Maybe; @@ -704,14 +722,6 @@ export interface AuthenticationsOverTimeData { totalCount: number; } -export interface MatrixOverTimeHistogramData { - x: number; - - y: number; - - g: string; -} - export interface TimelineData { edges: TimelineEdges[]; @@ -2127,6 +2137,13 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe; } +export interface AnomaliesOverTimeSourceArgs { + timerange: TimerangeInput; + + filterQuery?: Maybe; + + defaultIndex: string[]; +} export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2421,6 +2438,58 @@ export interface DeleteTimelineMutationArgs { // Documents // ==================================================== +export namespace GetAnomaliesOverTimeQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + defaultIndex: string[]; + filterQuery?: Maybe; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + AnomaliesOverTime: AnomaliesOverTime; + }; + + export type AnomaliesOverTime = { + __typename?: 'AnomaliesOverTimeData'; + + anomaliesOverTime: _AnomaliesOverTime[]; + + totalCount: number; + + inspect: Maybe; + }; + + export type _AnomaliesOverTime = { + __typename?: 'MatrixOverTimeHistogramData'; + + x: number; + + y: number; + + g: string; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + export namespace GetAuthenticationsOverTimeQuery { export type Variables = { sourceId: string; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx index 48b6d34d0b28b..1252c7031e8a5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx @@ -10,6 +10,8 @@ import { Route, Switch } from 'react-router-dom'; import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; import { Anomaly } from '../../../components/ml/types'; import { HostsTableType } from '../../../store/hosts/model'; +import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; +import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; import { HostDetailsTabsProps } from './types'; import { type } from './utils'; @@ -18,7 +20,6 @@ import { HostsQueryTabBody, AuthenticationsQueryTabBody, UncommonProcessQueryTabBody, - AnomaliesQueryTabBody, EventsQueryTabBody, } from '../navigation'; @@ -84,7 +85,9 @@ const HostDetailsTabs = React.memo( /> } + render={() => ( + + )} /> ( /> } + render={() => ( + + )} /> ( - -); - -AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts index 8a8f23208363d..f20138f520620 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './anomalies_query_tab_body'; export * from './authentications_query_tab_body'; export * from './events_query_tab_body'; export * from './hosts_query_tab_body'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts index d567038a05bd8..98d931dd7e275 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts @@ -25,6 +25,18 @@ type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPe export type HostsNavTab = Record; +export type SetQuery = ({ + id, + inspect, + loading, + refetch, +}: { + id: string; + inspect: InspectQuery | null; + loading: boolean; + refetch: Refetch; +}) => void; + interface QueryTabBodyProps { type: hostsModel.HostsType; startDate: number; @@ -32,30 +44,13 @@ interface QueryTabBodyProps { filterQuery?: string | ESTermQuery; } -export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { - skip: boolean; - narrowDateRange: NarrowDateRange; - hostName?: string; -}; - export type HostsComponentsQueryProps = QueryTabBodyProps & { deleteQuery?: ({ id }: { id: string }) => void; indexPattern: StaticIndexPattern; skip: boolean; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; - }) => void; + setQuery: SetQuery; updateDateRange?: UpdateDateRange; narrowDateRange?: NarrowDateRange; }; export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; -export type AnomaliesChildren = (args: AnomaliesQueryTabBodyProps) => JSX.Element; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx index 96111f0479938..477f435b84b20 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx @@ -39,6 +39,7 @@ import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; import { TlsQueryTable } from './tls_query_table'; import { IPDetailsComponentProps } from './types'; import { UsersQueryTable } from './users_query_table'; +import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; import { esQuery } from '../../../../../../../../src/plugins/data/public'; export { getBreadcrumbs } from './utils'; @@ -58,6 +59,7 @@ export const IPDetailsComponent = React.memo( setQuery, to, }) => { + const type = networkModel.NetworkType.details; const narrowDateRange = useCallback( (score, interval) => { const fromTo = scoreIntervalToDateTime(score, interval); @@ -108,7 +110,7 @@ export const IPDetailsComponent = React.memo( skip={isInitializing} sourceId="default" filterQuery={filterQuery} - type={networkModel.NetworkType.details} + type={type} ip={ip} > {({ id, inspect, ipOverviewData, loading, refetch }) => ( @@ -127,7 +129,7 @@ export const IPDetailsComponent = React.memo( anomaliesData={anomaliesData} loading={loading} isLoadingAnomaliesData={isLoadingAnomaliesData} - type={networkModel.NetworkType.details} + type={type} flowTarget={flowTarget} refetch={refetch} setQuery={setQuery} @@ -158,7 +160,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} indexPattern={indexPattern} /> @@ -172,7 +174,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} indexPattern={indexPattern} /> @@ -190,7 +192,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} indexPattern={indexPattern} /> @@ -204,7 +206,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} indexPattern={indexPattern} /> @@ -220,7 +222,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} /> @@ -232,7 +234,7 @@ export const IPDetailsComponent = React.memo( ip={ip} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} setQuery={setQuery} /> @@ -246,19 +248,23 @@ export const IPDetailsComponent = React.memo( setQuery={setQuery} skip={isInitializing} startDate={from} - type={networkModel.NetworkType.details} + type={type} /> - diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/anomalies_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/anomalies_query_tab_body.tsx deleted file mode 100644 index daf9cd2dd1d12..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/anomalies_query_tab_body.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { AnomaliesQueryTabBodyProps } from './types'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; - -export const AnomaliesQueryTabBody = ({ - to, - isInitializing, - from, - type, - narrowDateRange, -}: AnomaliesQueryTabBodyProps) => ( - -); - -AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx index 0fe370c144049..6ddd3bbec3a32 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx @@ -17,21 +17,21 @@ import { IPsQueryTabBodyProps as CountriesQueryTabBodyProps } from './types'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); export const CountriesQueryTabBody = ({ - to, + endDate, filterQuery, - isInitializing, - from, + skip, + startDate, setQuery, indexPattern, flowTarget, }: CountriesQueryTabBodyProps) => ( {({ diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx index 34ff35bd145a2..da3c2fcfbc67b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx @@ -12,28 +12,28 @@ import { NetworkDnsTable } from '../../../components/page/network/network_dns_ta import { NetworkDnsQuery, NetworkDnsHistogramQuery } from '../../../containers/network_dns'; import { manageQuery } from '../../../components/page/manage_query'; -import { DnsQueryTabBodyProps } from './types'; +import { NetworkComponentQueryProps } from './types'; import { NetworkDnsHistogram } from '../../../components/page/network/dns_histogram'; const NetworkDnsTableManage = manageQuery(NetworkDnsTable); const NetworkDnsHistogramManage = manageQuery(NetworkDnsHistogram); export const DnsQueryTabBody = ({ - to, + endDate, filterQuery, - isInitializing, - from, + skip, + startDate, setQuery, type, updateDateRange = () => {}, -}: DnsQueryTabBodyProps) => ( +}: NetworkComponentQueryProps) => ( <> {({ totalCount, loading, id, inspect, refetch, histogram }) => ( @@ -41,8 +41,8 @@ export const DnsQueryTabBody = ({ id={id} loading={loading} data={histogram} - endDate={to} - startDate={from} + endDate={endDate} + startDate={startDate} inspect={inspect} refetch={refetch} setQuery={setQuery} @@ -53,11 +53,11 @@ export const DnsQueryTabBody = ({ {({ diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx index a20a212623fb8..639a14d354ced 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx @@ -17,18 +17,18 @@ import { HttpQueryTabBodyProps } from './types'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); export const HttpQueryTabBody = ({ - to, + endDate, filterQuery, - isInitializing, - from, + skip, + startDate, setQuery, }: HttpQueryTabBodyProps) => ( {({ diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts index 9e8b4c6215031..44b78cb3077ff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './anomalies_query_tab_body'; export * from './network_routes'; export * from './network_routes_loading'; export * from './nav_tabs'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx index 08ba75443b333..95aaa90fe7865 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx @@ -17,21 +17,21 @@ import { IPsQueryTabBodyProps } from './types'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); export const IPsQueryTabBody = ({ - to, + endDate, filterQuery, - isInitializing, - from, + skip, + startDate, setQuery, indexPattern, flowTarget, }: IPsQueryTabBodyProps) => ( {({ diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index 0f373be94b45b..681e1f8e1e34d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -14,7 +14,8 @@ import { scoreIntervalToDateTime } from '../../../components/ml/score/score_inte import { IPsQueryTabBody } from './ips_query_tab_body'; import { CountriesQueryTabBody } from './countries_query_tab_body'; import { HttpQueryTabBody } from './http_query_tab_body'; -import { AnomaliesQueryTabBody } from './anomalies_query_tab_body'; +import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; import { DnsQueryTabBody } from './dns_query_tab_body'; import { ConditionalFlexGroup } from './conditional_flex_group'; import { NetworkRoutesProps, NetworkRouteType } from './types'; @@ -50,24 +51,44 @@ export const NetworkRoutes = ({ [from, to] ); - const tabProps = { - networkPagePath, + const networkAnomaliesFilterQuery = { + bool: { + should: [ + { + exists: { + field: 'source.ip', + }, + }, + { + exists: { + field: 'destination.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }; + + const commonProps = { + startDate: from, + endDate: to, + skip: isInitializing, type, - to, + narrowDateRange, + setQuery, filterQuery, - isInitializing, - from, + }; + + const tabProps = { + ...commonProps, indexPattern, - setQuery, updateDateRange, }; const anomaliesProps = { - from, - to, - isInitializing, - type, - narrowDateRange, + ...commonProps, + anomaliesFilterQuery: networkAnomaliesFilterQuery, + AnomaliesTableComponent: AnomaliesNetworkTable, }; return ( @@ -115,7 +136,12 @@ export const NetworkRoutes = ({ /> } + render={() => ( + + )} /> ); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx index 1f93e293be865..0adfec203e0a6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx @@ -13,23 +13,23 @@ import { TlsQueryTabBodyProps } from './types'; const TlsTableManage = manageQuery(TlsTable); export const TlsQueryTabBody = ({ - to, + endDate, filterQuery, flowTarget, ip = '', setQuery, - isInitializing, - from, + skip, + startDate, type, }: TlsQueryTabBodyProps) => ( {({ id, inspect, isInspected, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index 5f5f0a026d375..bc63e26f71eba 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -10,41 +10,37 @@ import { NavTab } from '../../../components/navigation/types'; import { FlowTargetSourceDest } from '../../../graphql/types'; import { networkModel } from '../../../store'; import { ESTermQuery } from '../../../../common/typed_json'; -import { NarrowDateRange } from '../../../components/ml/types'; import { GlobalTimeArgs } from '../../../containers/global_time'; import { SetAbsoluteRangeDatePicker } from '../types'; import { UpdateDateRange } from '../../../components/charts/common'; +import { NarrowDateRange } from '../../../components/ml/types'; -interface QueryTabBodyProps { +interface QueryTabBodyProps extends Pick { + skip: boolean; type: networkModel.NetworkType; + startDate: number; + endDate: number; filterQuery?: string | ESTermQuery; updateDateRange?: UpdateDateRange; narrowDateRange?: NarrowDateRange; } -export type DnsQueryTabBodyProps = QueryTabBodyProps & GlobalTimeArgs; +export type NetworkComponentQueryProps = QueryTabBodyProps; -export type IPsQueryTabBodyProps = QueryTabBodyProps & - GlobalTimeArgs & { - indexPattern: StaticIndexPattern; - flowTarget: FlowTargetSourceDest; - }; +export type IPsQueryTabBodyProps = QueryTabBodyProps & { + indexPattern: StaticIndexPattern; + flowTarget: FlowTargetSourceDest; +}; -export type TlsQueryTabBodyProps = QueryTabBodyProps & - GlobalTimeArgs & { - flowTarget: FlowTargetSourceDest; - ip?: string; - }; +export type TlsQueryTabBodyProps = QueryTabBodyProps & { + flowTarget: FlowTargetSourceDest; + ip?: string; +}; -export type HttpQueryTabBodyProps = QueryTabBodyProps & - GlobalTimeArgs & { - ip?: string; - }; -export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & - Pick & { - narrowDateRange: NarrowDateRange; - }; +export type HttpQueryTabBodyProps = QueryTabBodyProps & { + ip?: string; +}; export type NetworkRoutesProps = GlobalTimeArgs & { networkPagePath: string; diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts new file mode 100644 index 0000000000000..4bfd6be173105 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createAnomaliesResolvers } from './resolvers'; +export { anomaliesSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts new file mode 100644 index 0000000000000..47e227a8c0f84 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Anomalies } from '../../lib/anomalies'; +import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; +import { createOptions } from '../../utils/build_query/create_options'; +import { QuerySourceResolver } from '../sources/resolvers'; +import { SourceResolvers } from '../types'; + +export interface AnomaliesResolversDeps { + anomalies: Anomalies; +} + +type QueryAnomaliesOverTimeResolver = ChildResolverOf< + AppResolverOf, + QuerySourceResolver +>; + +export const createAnomaliesResolvers = ( + libs: AnomaliesResolversDeps +): { + Source: { + AnomaliesOverTime: QueryAnomaliesOverTimeResolver; + }; +} => ({ + Source: { + async AnomaliesOverTime(source, args, { req }, info) { + const options = { + ...createOptions(source, args, info), + defaultIndex: args.defaultIndex, + }; + return libs.anomalies.getAnomaliesOverTime(req, options); + }, + }, +}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts new file mode 100644 index 0000000000000..1dad0aafd55b0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const anomaliesSchema = gql` + type AnomaliesOverTimeData { + inspect: Inspect + anomaliesOverTime: [MatrixOverTimeHistogramData!]! + totalCount: Float! + } + + extend type Source { + AnomaliesOverTime( + timerange: TimerangeInput! + filterQuery: String + defaultIndex: [String!]! + ): AnomaliesOverTimeData! + } +`; diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index 110a390c19531..901d27295479a 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -7,6 +7,7 @@ import { rootSchema } from '../../common/graphql/root'; import { sharedSchema } from '../../common/graphql/shared'; +import { anomaliesSchema } from './anomalies'; import { authenticationsSchema } from './authentications'; import { ecsSchema } from './ecs'; import { eventsSchema } from './events'; @@ -29,6 +30,7 @@ import { tlsSchema } from './tls'; import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; export const schemas = [ + anomaliesSchema, authenticationsSchema, ecsSchema, eventsSchema, diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index 44cfc81339527..fda79ad543bf6 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -458,6 +458,8 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; + + AnomaliesOverTime: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; @@ -558,6 +560,28 @@ export interface IndexField { format?: Maybe; } +export interface AnomaliesOverTimeData { + inspect?: Maybe; + + anomaliesOverTime: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface Inspect { + dsl: string[]; + + response: string[]; +} + +export interface MatrixOverTimeHistogramData { + x: number; + + y: number; + + g: string; +} + export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -692,12 +716,6 @@ export interface PageInfoPaginated { showMorePagesIndicator: boolean; } -export interface Inspect { - dsl: string[]; - - response: string[]; -} - export interface AuthenticationsOverTimeData { inspect?: Maybe; @@ -706,14 +724,6 @@ export interface AuthenticationsOverTimeData { totalCount: number; } -export interface MatrixOverTimeHistogramData { - x: number; - - y: number; - - g: string; -} - export interface TimelineData { edges: TimelineEdges[]; @@ -2129,6 +2139,13 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe; } +export interface AnomaliesOverTimeSourceArgs { + timerange: TimerangeInput; + + filterQuery?: Maybe; + + defaultIndex: string[]; +} export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2763,6 +2780,8 @@ export namespace SourceResolvers { configuration?: ConfigurationResolver; /** The status of the source */ status?: StatusResolver; + + AnomaliesOverTime?: AnomaliesOverTimeResolver; /** Gets Authentication success and failures based on a timerange */ Authentications?: AuthenticationsResolver; @@ -2834,6 +2853,19 @@ export namespace SourceResolvers { Parent, TContext >; + export type AnomaliesOverTimeResolver< + R = AnomaliesOverTimeData, + Parent = Source, + TContext = SiemContext + > = Resolver; + export interface AnomaliesOverTimeArgs { + timerange: TimerangeInput; + + filterQuery?: Maybe; + + defaultIndex: string[]; + } + export type AuthenticationsResolver< R = AuthenticationsData, Parent = Source, @@ -3375,6 +3407,81 @@ export namespace IndexFieldResolvers { > = Resolver; } +export namespace AnomaliesOverTimeDataResolvers { + export interface Resolvers { + inspect?: InspectResolver, TypeParent, TContext>; + + anomaliesOverTime?: AnomaliesOverTimeResolver< + MatrixOverTimeHistogramData[], + TypeParent, + TContext + >; + + totalCount?: TotalCountResolver; + } + + export type InspectResolver< + R = Maybe, + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; + export type AnomaliesOverTimeResolver< + R = MatrixOverTimeHistogramData[], + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; + export type TotalCountResolver< + R = number, + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; +} + +export namespace InspectResolvers { + export interface Resolvers { + dsl?: DslResolver; + + response?: ResponseResolver; + } + + export type DslResolver = Resolver< + R, + Parent, + TContext + >; + export type ResponseResolver = Resolver< + R, + Parent, + TContext + >; +} + +export namespace MatrixOverTimeHistogramDataResolvers { + export interface Resolvers { + x?: XResolver; + + y?: YResolver; + + g?: GResolver; + } + + export type XResolver< + R = number, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver; + export type YResolver< + R = number, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver; + export type GResolver< + R = string, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver; +} + export namespace AuthenticationsDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -3820,25 +3927,6 @@ export namespace PageInfoPaginatedResolvers { > = Resolver; } -export namespace InspectResolvers { - export interface Resolvers { - dsl?: DslResolver; - - response?: ResponseResolver; - } - - export type DslResolver = Resolver< - R, - Parent, - TContext - >; - export type ResponseResolver = Resolver< - R, - Parent, - TContext - >; -} - export namespace AuthenticationsOverTimeDataResolvers { export interface Resolvers { inspect?: InspectResolver, TypeParent, TContext>; @@ -3869,32 +3957,6 @@ export namespace AuthenticationsOverTimeDataResolvers { > = Resolver; } -export namespace MatrixOverTimeHistogramDataResolvers { - export interface Resolvers { - x?: XResolver; - - y?: YResolver; - - g?: GResolver; - } - - export type XResolver< - R = number, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver; - export type YResolver< - R = number, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver; - export type GResolver< - R = string, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver; -} - export namespace TimelineDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -8645,6 +8707,9 @@ export type IResolvers = { SourceFields?: SourceFieldsResolvers.Resolvers; SourceStatus?: SourceStatusResolvers.Resolvers; IndexField?: IndexFieldResolvers.Resolvers; + AnomaliesOverTimeData?: AnomaliesOverTimeDataResolvers.Resolvers; + Inspect?: InspectResolvers.Resolvers; + MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers; AuthenticationsData?: AuthenticationsDataResolvers.Resolvers; AuthenticationsEdges?: AuthenticationsEdgesResolvers.Resolvers; AuthenticationItem?: AuthenticationItemResolvers.Resolvers; @@ -8657,9 +8722,7 @@ export type IResolvers = { OsEcsFields?: OsEcsFieldsResolvers.Resolvers; CursorType?: CursorTypeResolvers.Resolvers; PageInfoPaginated?: PageInfoPaginatedResolvers.Resolvers; - Inspect?: InspectResolvers.Resolvers; AuthenticationsOverTimeData?: AuthenticationsOverTimeDataResolvers.Resolvers; - MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers; TimelineData?: TimelineDataResolvers.Resolvers; TimelineEdges?: TimelineEdgesResolvers.Resolvers; TimelineItem?: TimelineItemResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index b040b773c1e53..08c481164d539 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -6,6 +6,7 @@ import { IResolvers, makeExecutableSchema } from 'graphql-tools'; import { schemas } from './graphql'; +import { createAnomaliesResolvers } from './graphql/anomalies'; import { createAuthenticationsResolvers } from './graphql/authentications'; import { createScalarToStringArrayValueResolvers } from './graphql/ecs'; import { createEsValueResolvers, createEventsResolvers } from './graphql/events'; @@ -32,6 +33,7 @@ import { createTlsResolvers } from './graphql/tls'; export const initServer = (libs: AppBackendLibs) => { const schema = makeExecutableSchema({ resolvers: [ + createAnomaliesResolvers(libs) as IResolvers, createAuthenticationsResolvers(libs) as IResolvers, createEsValueResolvers() as IResolvers, createEventsResolvers(libs) as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts new file mode 100644 index 0000000000000..f4b7aff4854e5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { AnomaliesOverTimeData } from '../../graphql/types'; +import { inspectStringifyObject } from '../../utils/build_query'; +import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework'; +import { TermAggregation } from '../types'; + +import { AnomalyHit, AnomaliesAdapter, AnomaliesActionGroupData } from './types'; +import { buildAnomaliesOverTimeQuery } from './query.anomalies_over_time.dsl'; +import { MatrixOverTimeHistogramData } from '../../../public/graphql/types'; + +export class ElasticsearchAnomaliesAdapter implements AnomaliesAdapter { + constructor(private readonly framework: FrameworkAdapter) {} + + public async getAnomaliesOverTime( + request: FrameworkRequest, + options: RequestBasicOptions + ): Promise { + const dsl = buildAnomaliesOverTimeQuery(options); + + const response = await this.framework.callWithRequest( + request, + 'search', + dsl + ); + + const totalCount = getOr(0, 'hits.total.value', response); + const anomaliesOverTimeBucket = getOr([], 'aggregations.anomalyActionGroup.buckets', response); + + const inspect = { + dsl: [inspectStringifyObject(dsl)], + response: [inspectStringifyObject(response)], + }; + return { + inspect, + anomaliesOverTime: getAnomaliesOverTimeByJobId(anomaliesOverTimeBucket), + totalCount, + }; + } +} + +const getAnomaliesOverTimeByJobId = ( + data: AnomaliesActionGroupData[] +): MatrixOverTimeHistogramData[] => { + let result: MatrixOverTimeHistogramData[] = []; + data.forEach(({ key: group, anomalies }) => { + const anomaliesData = getOr([], 'buckets', anomalies).map( + ({ key, doc_count }: { key: number; doc_count: number }) => ({ + x: key, + y: doc_count, + g: group, + }) + ); + result = [...result, ...anomaliesData]; + }); + + return result; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts new file mode 100644 index 0000000000000..7beeea4ad9e4e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FrameworkRequest, RequestBasicOptions } from '../framework'; +export * from './elasticsearch_adapter'; +import { AnomaliesAdapter } from './types'; +import { AnomaliesOverTimeData } from '../../../public/graphql/types'; + +export class Anomalies { + constructor(private readonly adapter: AnomaliesAdapter) {} + + public async getAnomaliesOverTime( + req: FrameworkRequest, + options: RequestBasicOptions + ): Promise { + return this.adapter.getAnomaliesOverTime(req, options); + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts new file mode 100644 index 0000000000000..34a6a6a8f601f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createQueryFilterClauses, calculateTimeseriesInterval } from '../../utils/build_query'; +import { RequestBasicOptions } from '../framework'; + +export const buildAnomaliesOverTimeQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: RequestBasicOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + timestamp: { + gte: from, + lte: to, + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeseriesInterval(from, to); + const histogramTimestampField = 'timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: `${interval}s`, + }, + }; + const autoDateHistogram = { + auto_date_histogram: { + field: histogramTimestampField, + buckets: 36, + }, + }; + return { + anomalyActionGroup: { + terms: { + field: 'job_id', + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + anomalies: interval ? dateHistogram : autoDateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggs: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts new file mode 100644 index 0000000000000..1e13ad88f8af3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AnomaliesOverTimeData } from '../../graphql/types'; +import { FrameworkRequest, RequestBasicOptions } from '../framework'; +import { SearchHit } from '../types'; + +export interface AnomaliesAdapter { + getAnomaliesOverTime( + req: FrameworkRequest, + options: RequestBasicOptions + ): Promise; +} + +export interface AnomalySource { + [field: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface AnomalyHit extends SearchHit { + sort: string[]; + _source: AnomalySource; + aggregations: { + [agg: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + }; +} + +interface AnomaliesOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AnomaliesActionGroupData { + key: number; + anomalies: { + bucket: AnomaliesOverTimeHistogramData[]; + }; + doc_count: number; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts index e2ff0013e063c..a6b788cb70657 100644 --- a/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts @@ -27,8 +27,7 @@ export const buildAuthenticationsOverTimeQuery = ({ ]; const getHistogramAggregation = () => { - const minIntervalSeconds = 10; - const interval = calculateTimeseriesInterval(from, to, minIntervalSeconds); + const interval = calculateTimeseriesInterval(from, to); const histogramTimestampField = '@timestamp'; const dateHistogram = { date_histogram: { diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index c09db5bce5cc2..6e0c5e98206e4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -6,6 +6,8 @@ import { EnvironmentMode } from 'src/core/server'; import { ServerFacade } from '../../types'; +import { Anomalies } from '../anomalies'; +import { ElasticsearchAnomaliesAdapter } from '../anomalies/elasticsearch_adapter'; import { Authentications } from '../authentications'; import { ElasticsearchAuthenticationAdapter } from '../authentications/elasticsearch_adapter'; import { KibanaConfigurationAdapter } from '../configuration/kibana_configuration_adapter'; @@ -43,6 +45,7 @@ export function compose(server: ServerFacade, mode: EnvironmentMode): AppBackend const pinnedEvent = new PinnedEvent({ savedObjects: framework.getSavedObjectsService() }); const domainLibs: AppDomainLibs = { + anomalies: new Anomalies(new ElasticsearchAnomaliesAdapter(framework)), authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), diff --git a/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts index e655485638e16..98bd6944c1b51 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts @@ -27,8 +27,7 @@ export const buildEventsOverTimeQuery = ({ ]; const getHistogramAggregation = () => { - const minIntervalSeconds = 10; - const interval = calculateTimeseriesInterval(from, to, minIntervalSeconds); + const interval = calculateTimeseriesInterval(from, to); const histogramTimestampField = '@timestamp'; const dateHistogram = { date_histogram: { diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index a5429ebf76517..13d040b969545 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Anomalies } from './anomalies'; import { Authentications } from './authentications'; import { ConfigurationAdapter } from './configuration'; import { Events } from './events'; @@ -27,6 +28,7 @@ import { SignalAlertParamsRest } from './detection_engine/alerts/types'; export * from './hosts'; export interface AppDomainLibs { + anomalies: Anomalies; authentications: Authentications; events: Events; fields: IndexFields; diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts b/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts index 3eaaa6c30a4fa..752c686b243ac 100644 --- a/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts +++ b/x-pack/legacy/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts @@ -91,8 +91,7 @@ export const calculateAuto = { export const calculateTimeseriesInterval = ( lowerBoundInMsSinceEpoch: number, - upperBoundInMsSinceEpoch: number, - minIntervalSeconds: number + upperBoundInMsSinceEpoch: number ) => { const duration = moment.duration(upperBoundInMsSinceEpoch - lowerBoundInMsSinceEpoch, 'ms'); diff --git a/yarn.lock b/yarn.lock index 68b9a74829281..64d33426d7aa4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9719,15 +9719,10 @@ deep-object-diff@^1.1.0: resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a" integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw== -deepmerge@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e" - integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow== - -deepmerge@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.0.0.tgz#3e3110ca29205f120d7cb064960a39c3d2087c09" - integrity sha512-YZ1rOP5+kHor4hMAH+HRQnBQHg+wvS1un1hAOuIcxcBy0hzcUf6Jg2a1w65kpoOUnurOfZbERwjI1TfZxNjcww== +deepmerge@3.2.0, deepmerge@^4.0.0, deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== default-compare@^1.0.0: version "1.0.0" From 59affbaaf9c921ff5e269c99ec49aae120f8f92e Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 25 Nov 2019 11:27:54 +0100 Subject: [PATCH 042/128] [SIEM] Returns failure if some tests fails (#51439) * returns failure if some tests fails * Update x-pack/legacy/plugins/siem/package.json Co-Authored-By: Spencer --- x-pack/legacy/plugins/siem/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index ca5fefe52bcc4..29c26c5f674e3 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -7,7 +7,7 @@ "scripts": { "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "../../../node_modules/.bin/cypress open", - "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/" + "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; status=$?; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/ && exit $status;" }, "devDependencies": { "@types/lodash": "^4.14.110", From a1a256ddcbea6f120d06f29c603a96e4f5ea1689 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 25 Nov 2019 12:20:48 +0100 Subject: [PATCH 043/128] [ML] Reactive time-range selection in SMV (#51008) * [ML] http service to TS, add httpCall using fromFetch * [ML] types, add esSearchRx * [ML] timeresiesexplorer_contans to ts * [ML] timeseries_search_service to TS, add getMetricDataRx * [ML] result service with observables * [ML] update resolvers, forecast data support * [ML] wip timeseriesexplorer * [ML] fix state update for zoom * [ML] skip loading update * [ML] cleanup contextChartSelected * [ML] add to subscriptions * [ML] update imports * [ML] timeseriesexplorer_utils * [ML] refactor result service * [ML] getAnnotations * [ML] rename subject * [ML] fix explorer and unit tests * [ML] fix forecast * [ML] replace skipWhilte with filter * [ML] rename http$ * [ML] rename esSearch$ * [ML] remove filter operator, check for contextChartData before calculating the default range * [ML] remove casting for FocusData * [ML] replace with an arrow function * [ML] fix Job import path * [ML] fix annotations --- .../plugins/ml/common/util/job_utils.d.ts | 4 + .../annotations_table/annotations_table.js | 2 +- .../annotations_table.test.js | 16 +- .../explorer_charts_container_service.js | 8 +- .../explorer_charts_container_service.test.js | 55 +- .../application/explorer/explorer_utils.js | 4 +- .../common/results_loader/results_loader.ts | 20 +- .../services/forecast_service.d.ts | 26 + .../application/services/forecast_service.js | 186 +++--- .../application/services/http_service.js | 50 -- .../application/services/http_service.ts | 89 +++ .../{annotations.js => annotations.ts} | 27 +- .../services/ml_api_service/index.d.ts | 5 + .../services/ml_api_service/index.js | 9 +- .../services/ml_api_service/results.js | 8 +- .../application/services/results_service.d.ts | 61 -- .../services/results_service/index.ts | 55 ++ .../results_service/result_service_rx.ts | 534 +++++++++++++++++ .../results_service/results_service.d.ts | 37 ++ .../{ => results_service}/results_service.js | 564 +----------------- .../timeseries_chart/timeseries_chart.js | 4 - .../application/timeseriesexplorer/index.js | 6 +- ...ervice.js => timeseries_search_service.ts} | 92 +-- .../timeseriesexplorer/timeseriesexplorer.js | 329 ++++++---- ...nts.js => timeseriesexplorer_constants.ts} | 1 - .../get_focus_data.ts | 164 +++++ .../timeseriesexplorer_utils/index.ts | 8 + .../timeseriesexplorer_utils.d.ts | 52 ++ .../timeseriesexplorer_utils.js | 170 +----- 29 files changed, 1429 insertions(+), 1157 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/services/http_service.js create mode 100644 x-pack/legacy/plugins/ml/public/application/services/http_service.ts rename x-pack/legacy/plugins/ml/public/application/services/ml_api_service/{annotations.js => annotations.ts} (55%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts rename x-pack/legacy/plugins/ml/public/application/services/{ => results_service}/results_service.js (70%) rename x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/{timeseries_search_service.js => timeseries_search_service.ts} (69%) rename x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/{timeseriesexplorer_constants.js => timeseriesexplorer_constants.ts} (99%) create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts rename x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/{ => timeseriesexplorer_utils}/timeseriesexplorer_utils.js (70%) diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts index 23152afe0af2f..df62d19b6d27b 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Job } from '../../public/application/jobs/new_job/common/job_creator/configs'; + export interface ValidationMessage { id: string; } @@ -39,3 +41,5 @@ export function validateModelMemoryLimitUnits( export function processCreatedBy(customSettings: { created_by?: string }): void; export function mlFunctionToESAggregation(functionName: string): string | null; + +export function isModelPlotEnabled(job: Job, detectorIndex: number, entityFields: any[]): boolean; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 3e5afd3c1e7e7..909abfd4abc23 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -87,7 +87,7 @@ const AnnotationsTable = injectI18n(class AnnotationsTable extends Component { earliestMs: null, latestMs: null, maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { + }).toPromise().then((resp) => { this.setState((prevState, props) => ({ annotations: resp.annotations[props.jobs[0].job_id] || [], errorMessage: undefined, diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 1ed30c7e13727..c3ca28dc96bfc 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -24,13 +24,17 @@ jest.mock('../../../services/job_service', () => ({ } })); -jest.mock('../../../services/ml_api_service', () => ({ - ml: { - annotations: { - getAnnotations: jest.fn().mockResolvedValue({ annotations: [] }) +jest.mock('../../../services/ml_api_service', () => { + const { of } = require('rxjs'); + const mockAnnotations$ = of({ annotations: [] }); + return { + ml: { + annotations: { + getAnnotations: jest.fn().mockReturnValue(mockAnnotations$) + } } - } -})); + };} +); describe('AnnotationsTable', () => { test('Minimal initialization without props.', () => { diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index b222b6e1160c6..01afd9ffb602f 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -124,7 +124,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, config.interval - ); + ).toPromise(); } else { // Extract the partition, by, over fields on which to filter. const criteriaFields = []; @@ -169,7 +169,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, interval - ) + ).toPromise() .then((resp) => { // Return data in format required by the explorer charts. const results = resp.results; @@ -201,7 +201,7 @@ export function explorerChartsContainerServiceFactory(callback) { range.min, range.max, ANOMALIES_MAX_RESULTS - ); + ).toPromise(); } // Query 3 - load any scheduled events for the job. @@ -213,7 +213,7 @@ export function explorerChartsContainerServiceFactory(callback) { config.interval, 1, MAX_SCHEDULED_EVENTS - ); + ).toPromise(); } // Query 4 - load context data distribution diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js index b907cd92df10c..f8ed067a3de54 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -47,36 +47,39 @@ jest.mock('../../services/job_service', () => ({ } })); -jest.mock('../../services/results_service', () => ({ - mlResultsService: { - getMetricData(indices) { +jest.mock('../../services/results_service', () => { + const { of } = require('rxjs'); + return { + mlResultsService: { + getMetricData(indices) { // this is for 'call anomalyChangeListener with actual series config' - if (indices[0] === 'farequote-2017') { - return Promise.resolve(mockSeriesPromisesResponse[0][0]); - } - // this is for 'filtering should skip values of null' - return Promise.resolve(mockMetricClone); - }, - getRecordsForCriteria() { - return Promise.resolve(mockSeriesPromisesResponse[0][1]); - }, - getScheduledEventsByBucket() { - return Promise.resolve(mockSeriesPromisesResponse[0][2]); - }, - getEventDistributionData(indices) { + if (indices[0] === 'farequote-2017') { + return of(mockSeriesPromisesResponse[0][0]); + } + // this is for 'filtering should skip values of null' + return of(mockMetricClone); + }, + getRecordsForCriteria() { + return of(mockSeriesPromisesResponse[0][1]); + }, + getScheduledEventsByBucket() { + return of(mockSeriesPromisesResponse[0][2]); + }, + getEventDistributionData(indices) { // this is for 'call anomalyChangeListener with actual series config' - if (indices[0] === 'farequote-2017') { - return Promise.resolve([]); + if (indices[0] === 'farequote-2017') { + return Promise.resolve([]); + } + // this is for 'filtering should skip values of null' and + // resolves with a dummy object to trigger the processing + // of the event distribution chartdata filtering + return Promise.resolve([{ + entity: 'mock' + }]); } - // this is for 'filtering should skip values of null' and - // resolves with a dummy object to trigger the processing - // of the event distribution chartdata filtering - return Promise.resolve([{ - entity: 'mock' - }]); } - } -})); + }; +}); jest.mock('../../util/string_utils', () => ({ mlEscape(d) { return d; } diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js index 9c2d2041566e1..5ca8681d16749 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js @@ -417,7 +417,7 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { + }).toPromise().then((resp) => { if (resp.error !== undefined || resp.annotations === undefined) { return resolve([]); } @@ -477,7 +477,7 @@ export async function loadAnomaliesTableData( ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, MAX_CATEGORY_EXAMPLES, influencersFilterQuery - ).then((resp) => { + ).toPromise().then((resp) => { const anomalies = resp.anomalies; const detectorsByJob = mlJobService.detectorsByJob; anomalies.forEach((anomaly) => { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index d434e1be42e66..82808ef3d37ee 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -150,15 +150,17 @@ export class ResultsLoader { if (agg === null) { return { [dtrIndex]: [emptyModelItem] }; } - const resp = await mlResultsService.getModelPlotOutput( - this._jobCreator.jobId, - dtrIndex, - [], - this._lastModelTimeStamp, - this._jobCreator.end, - `${this._chartInterval.getInterval().asMilliseconds()}ms`, - agg.mlModelPlotAgg - ); + const resp = await mlResultsService + .getModelPlotOutput( + this._jobCreator.jobId, + dtrIndex, + [], + this._lastModelTimeStamp, + this._jobCreator.end, + `${this._chartInterval.getInterval().asMilliseconds()}ms`, + agg.mlModelPlotAgg + ) + .toPromise(); return this._createModel(resp, dtrIndex); } diff --git a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts new file mode 100644 index 0000000000000..19f77d97a5708 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { Job } from '../jobs/new_job/common/job_creator/configs'; + +export interface ForecastData { + success: boolean; + results: any; +} + +export const mlForecastService: { + getForecastData: ( + job: Job, + detectorIndex: number, + forecastId: string, + entityFields: any[], + earliestMs: number, + latestMs: number, + interval: string, + aggType: any + ) => Observable; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js index c420cca579c9c..4b6ce19b5e6c6 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.js @@ -9,6 +9,7 @@ // Service for carrying out requests to run ML forecasts and to obtain // data on forecasts that have been performed. import _ from 'lodash'; +import { map } from 'rxjs/operators'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { ml } from './ml_api_service'; @@ -192,117 +193,112 @@ function getForecastData( } } - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID, forecast ID, detector index, result type and time range. - const filterCriteria = [{ - query_string: { - query: 'result_type:model_forecast', - analyze_wildcard: true - } - }, - { - term: { job_id: job.job_id } - }, - { - term: { forecast_id: forecastId } - }, - { - term: { detector_index: detectorIndex } - }, - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } + const obj = { + success: true, + results: {} + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID, forecast ID, detector index, result type and time range. + const filterCriteria = [{ + query_string: { + query: 'result_type:model_forecast', + analyze_wildcard: true + } + }, + { + term: { job_id: job.job_id } + }, + { + term: { forecast_id: forecastId } + }, + { + term: { detector_index: detectorIndex } + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis' } - }]; + } + }]; - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - filterCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, (criteria) => { + filterCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue + } }); + }); - // If an aggType object has been passed in, use it. - // Otherwise default to avg, min and max aggs for the - // forecast prediction, upper and lower - const forecastAggs = (aggType === undefined) ? - { avg: 'avg', max: 'max', min: 'min' } : - { - avg: aggType.avg, - max: aggType.max, - min: aggType.min - }; + // If an aggType object has been passed in, use it. + // Otherwise default to avg, min and max aggs for the + // forecast prediction, upper and lower + const forecastAggs = (aggType === undefined) ? + { avg: 'avg', max: 'max', min: 'min' } : + { + avg: aggType.avg, + max: aggType.max, + min: aggType.min + }; - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: filterCriteria - } - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 1 + return ml.esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: filterCriteria + } + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval: interval, + min_doc_count: 1 + }, + aggs: { + prediction: { + [forecastAggs.avg]: { + field: 'forecast_prediction' + } + }, + forecastUpper: { + [forecastAggs.max]: { + field: 'forecast_upper' + } }, - aggs: { - prediction: { - [forecastAggs.avg]: { - field: 'forecast_prediction' - } - }, - forecastUpper: { - [forecastAggs.max]: { - field: 'forecast_upper' - } - }, - forecastLower: { - [forecastAggs.min]: { - field: 'forecast_lower' - } + forecastLower: { + [forecastAggs.min]: { + field: 'forecast_lower' } } } } } - }) - .then((resp) => { - const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); - _.each(aggregationsByTime, (dataForTime) => { - const time = dataForTime.key; - obj.results[time] = { - prediction: _.get(dataForTime, ['prediction', 'value']), - forecastUpper: _.get(dataForTime, ['forecastUpper', 'value']), - forecastLower: _.get(dataForTime, ['forecastLower', 'value']) - }; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); + } + }).pipe( + map(resp => { + const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); + _.each(aggregationsByTime, (dataForTime) => { + const time = dataForTime.key; + obj.results[time] = { + prediction: _.get(dataForTime, ['prediction', 'value']), + forecastUpper: _.get(dataForTime, ['forecastUpper', 'value']), + forecastLower: _.get(dataForTime, ['forecastLower', 'value']) + }; }); - }); + return obj; + }) + ); } // Runs a forecast diff --git a/x-pack/legacy/plugins/ml/public/application/services/http_service.js b/x-pack/legacy/plugins/ml/public/application/services/http_service.js deleted file mode 100644 index f0bef4396e4f3..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/http_service.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -// service for interacting with the server - -import chrome from 'ui/chrome'; - -import { addSystemApiHeader } from 'ui/system_api'; - -export function http(options) { - return new Promise((resolve, reject) => { - if(options && options.url) { - let url = ''; - url = url + (options.url || ''); - const headers = addSystemApiHeader({ - 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), - ...options.headers - }); - - const allHeaders = (options.headers === undefined) ? headers : { ...options.headers, ...headers }; - const body = (options.data === undefined) ? null : JSON.stringify(options.data); - - const payload = { - method: (options.method || 'GET'), - headers: allHeaders, - credentials: 'same-origin' - }; - - if (body !== null) { - payload.body = body; - } - - fetch(url, payload) - .then((resp) => { - resp.json().then((resp.ok === true) ? resolve : reject); - }) - .catch((resp) => { - reject(resp); - }); - } else { - reject(); - } - }); -} diff --git a/x-pack/legacy/plugins/ml/public/application/services/http_service.ts b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts new file mode 100644 index 0000000000000..1d68ec5b886eb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/http_service.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// service for interacting with the server + +import chrome from 'ui/chrome'; + +// @ts-ignore +import { addSystemApiHeader } from 'ui/system_api'; +import { fromFetch } from 'rxjs/fetch'; +import { from, Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +export interface HttpOptions { + url?: string; +} + +function getResultHeaders(headers: HeadersInit): HeadersInit { + return addSystemApiHeader({ + 'Content-Type': 'application/json', + 'kbn-version': chrome.getXsrfToken(), + ...headers, + }); +} + +export function http(options: any) { + return new Promise((resolve, reject) => { + if (options && options.url) { + let url = ''; + url = url + (options.url || ''); + const headers: Record = addSystemApiHeader({ + 'Content-Type': 'application/json', + 'kbn-version': chrome.getXsrfToken(), + ...options.headers, + }); + + const allHeaders = + options.headers === undefined ? headers : { ...options.headers, ...headers }; + const body = options.data === undefined ? null : JSON.stringify(options.data); + + const payload: RequestInit = { + method: options.method || 'GET', + headers: allHeaders, + credentials: 'same-origin', + }; + + if (body !== null) { + payload.body = body; + } + + fetch(url, payload) + .then(resp => { + resp.json().then(resp.ok === true ? resolve : reject); + }) + .catch(resp => { + reject(resp); + }); + } else { + reject(); + } + }); +} + +interface RequestOptions extends RequestInit { + body: BodyInit | any; +} + +export function http$(url: string, options: RequestOptions): Observable { + const requestInit: RequestInit = { + ...options, + credentials: 'same-origin', + method: options.method || 'GET', + ...(options.body ? { body: JSON.stringify(options.body) as string } : {}), + headers: getResultHeaders(options.headers ?? {}), + }; + + return fromFetch(url, requestInit).pipe( + switchMap(response => { + if (response.ok) { + return from(response.json() as Promise); + } else { + throw new Error(String(response.status)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts similarity index 55% rename from x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js rename to x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts index 560c4c460e118..54d55159646f6 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/annotations.ts @@ -4,33 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ - - import chrome from 'ui/chrome'; -import { http } from '../http_service'; +import { Annotation } from '../../../../common/types/annotations'; +import { http, http$ } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); export const annotations = { - getAnnotations(obj) { - return http({ - url: `${basePath}/annotations`, + getAnnotations(obj: { + jobIds: string[]; + earliestMs: number; + latestMs: number; + maxAnnotations: number; + }) { + return http$<{ annotations: Record }>(`${basePath}/annotations`, { method: 'POST', - data: obj + body: obj, }); }, - indexAnnotation(obj) { + indexAnnotation(obj: any) { return http({ url: `${basePath}/annotations/index`, method: 'PUT', - data: obj + data: obj, }); }, - deleteAnnotation(id) { + deleteAnnotation(id: string) { return http({ url: `${basePath}/annotations/delete/${id}`, - method: 'DELETE' + method: 'DELETE', }); - } + }, }; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index 11c65851270eb..7c0b22b0e1966 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Observable } from 'rxjs'; import { Annotation } from '../../../../common/types/annotations'; import { AggFieldNamePair } from '../../../../common/types/fields'; import { ExistingJobsAndGroups } from '../job_service'; @@ -15,6 +16,7 @@ import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analyt import { JobMessage } from '../../../../common/types/audit_message'; import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/analytics'; import { DeepPartial } from '../../../../common/types/common'; +import { annotations } from './annotations'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -65,6 +67,7 @@ declare interface Ml { annotations: { deleteAnnotation(id: string | undefined): Promise; indexAnnotation(annotation: Annotation): Promise; + getAnnotations: typeof annotations.getAnnotations; }; dataFrameAnalytics: { @@ -92,6 +95,7 @@ declare interface Ml { getJobStats(obj: object): Promise; getDatafeedStats(obj: object): Promise; esSearch(obj: object): any; + esSearch$(obj: object): Observable; getIndices(): Promise; dataRecognizerModuleJobsExist(obj: { moduleId: string }): Promise; getDataRecognizerModule(obj: { moduleId: string }): Promise; @@ -159,6 +163,7 @@ declare interface Ml { mlNodeCount(): Promise<{ count: number }>; mlInfo(): Promise; + getCardinalityOfFields(obj: Record): any; } declare const ml: Ml; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js index b3310eb6bcd53..34d9f9ec16f83 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js @@ -9,7 +9,7 @@ import { pick } from 'lodash'; import chrome from 'ui/chrome'; -import { http } from '../http_service'; +import { http, http$ } from '../http_service'; import { annotations } from './annotations'; import { dataFrameAnalytics } from './data_frame_analytics'; @@ -444,6 +444,13 @@ export const ml = { }); }, + esSearch$(obj) { + return http$(`${basePath}/es_search`, { + method: 'POST', + body: obj + }); + }, + getIndices() { const tempBasePath = chrome.addBasePath('/api'); return http({ diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js index 4bfec7643cecc..f9874cca840a7 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/results.js @@ -8,7 +8,7 @@ import chrome from 'ui/chrome'; -import { http } from '../http_service'; +import { http, http$ } from '../http_service'; const basePath = chrome.addBasePath('/api/ml'); @@ -25,11 +25,9 @@ export const results = { maxRecords, maxExamples, influencersFilterQuery) { - - return http({ - url: `${basePath}/results/anomalies_table_data`, + return http$(`${basePath}/results/anomalies_table_data`, { method: 'POST', - data: { + body: { jobIds, criteriaFields, influencers, diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts deleted file mode 100644 index 2bbe37c3fc05d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/results_service.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -type time = string; -export interface ModelPlotOutputResults { - results: Record; -} - -declare interface MlResultsService { - getScoresByBucket: ( - jobIds: string[], - earliestMs: number, - latestMs: number, - interval: string | number, - maxResults: number - ) => Promise; - getScheduledEventsByBucket: () => Promise; - getTopInfluencers: () => Promise; - getTopInfluencerValues: () => Promise; - getOverallBucketScores: ( - jobIds: any, - topN: any, - earliestMs: any, - latestMs: any, - interval?: any - ) => Promise; - getInfluencerValueMaxScoreByTime: () => Promise; - getRecordInfluencers: () => Promise; - getRecordsForInfluencer: () => Promise; - getRecordsForDetector: () => Promise; - getRecords: () => Promise; - getRecordsForCriteria: () => Promise; - getMetricData: () => Promise; - getEventRateData: ( - index: string, - query: any, - timeFieldName: string, - earliestMs: number, - latestMs: number, - interval: string | number - ) => Promise; - getEventDistributionData: () => Promise; - getModelPlotOutput: ( - jobId: string, - detectorIndex: number, - criteriaFields: string[], - earliestMs: number, - latestMs: number, - interval: string | number, - aggType: { - min: string; - max: string; - } - ) => Promise; - getRecordMaxScoreByTime: () => Promise; -} - -export const mlResultsService: MlResultsService; diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts new file mode 100644 index 0000000000000..9ab14aa7495a7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/index.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getMetricData, + getModelPlotOutput, + getRecordsForCriteria, + getScheduledEventsByBucket, +} from './result_service_rx'; +import { + getEventDistributionData, + getEventRateData, + getInfluencerValueMaxScoreByTime, + getOverallBucketScores, + getRecordInfluencers, + getRecordMaxScoreByTime, + getRecords, + getRecordsForDetector, + getRecordsForInfluencer, + getScoresByBucket, + getTopInfluencers, + getTopInfluencerValues, +} from './results_service'; + +export const mlResultsService = { + getScoresByBucket, + getScheduledEventsByBucket, + getTopInfluencers, + getTopInfluencerValues, + getOverallBucketScores, + getInfluencerValueMaxScoreByTime, + getRecordInfluencers, + getRecordsForInfluencer, + getRecordsForDetector, + getRecords, + getRecordsForCriteria, + getMetricData, + getEventRateData, + getEventDistributionData, + getModelPlotOutput, + getRecordMaxScoreByTime, +}; + +type time = string; +export interface ModelPlotOutputResults { + results: Record; +} + +export interface CriteriaField { + fieldName: string; + fieldValue: any; +} diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts new file mode 100644 index 0000000000000..2341ae15a3378 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -0,0 +1,534 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Queries Elasticsearch to obtain metric aggregation results. +// index can be a String, or String[], of index names to search. +// entityFields parameter must be an array, with each object in the array having 'fieldName' +// and 'fieldValue' properties. +// Extra query object can be supplied, or pass null if no additional query +// to that built from the supplied entity fields. +// Returned response contains a results property containing the requested aggregation. +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import _ from 'lodash'; +import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { ml } from '../ml_api_service'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; +import { CriteriaField } from './index'; + +interface ResultResponse { + success: boolean; +} + +export interface MetricData extends ResultResponse { + results: Record; +} + +export function getMetricData( + index: string, + entityFields: any[], + query: object | undefined, + metricFunction: string, // ES aggregation name + metricFieldName: string, + timeFieldName: string, + earliestMs: number, + latestMs: number, + interval: string +): Observable { + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, entity fields, + // plus any additional supplied query. + const shouldCriteria: object[] = []; + const mustCriteria: object[] = [ + { + range: { + [timeFieldName]: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + ...(query ? [query] : []), + ]; + + entityFields.forEach(entity => { + if (entity.fieldValue.length !== 0) { + mustCriteria.push({ + term: { + [entity.fieldName]: entity.fieldValue, + }, + }); + } else { + // Add special handling for blank entity field values, checking for either + // an empty string or the field not existing. + shouldCriteria.push({ + bool: { + must: [ + { + term: { + [entity.fieldName]: '', + }, + }, + ], + }, + }); + shouldCriteria.push({ + bool: { + must_not: [ + { + exists: { field: entity.fieldName }, + }, + ], + }, + }); + } + }); + + const body: any = { + query: { + bool: { + must: mustCriteria, + }, + }, + size: 0, + _source: { + excludes: [], + }, + aggs: { + byTime: { + date_histogram: { + field: timeFieldName, + interval, + min_doc_count: 0, + }, + }, + }, + }; + + if (shouldCriteria.length > 0) { + body.query.bool.should = shouldCriteria; + body.query.bool.minimum_should_match = shouldCriteria.length / 2; + } + + if (metricFieldName !== undefined && metricFieldName !== '') { + body.aggs.byTime.aggs = {}; + + const metricAgg: any = { + [metricFunction]: { + field: metricFieldName, + }, + }; + + if (metricFunction === 'percentiles') { + metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; + } + body.aggs.byTime.aggs.metric = metricAgg; + } + + return ml.esSearch$({ index, body }).pipe( + map((resp: any) => { + const obj: MetricData = { success: true, results: {} }; + const dataByTime = resp?.aggregations?.byTime?.buckets ?? []; + dataByTime.forEach((dataForTime: any) => { + if (metricFunction === 'count') { + obj.results[dataForTime.key] = dataForTime.doc_count; + } else { + const value = dataForTime?.metric?.value; + const values = dataForTime?.metric?.values; + if (dataForTime.doc_count === 0) { + obj.results[dataForTime.key] = null; + } else if (value !== undefined) { + obj.results[dataForTime.key] = value; + } else if (values !== undefined) { + // Percentiles agg currently returns NaN rather than null when none of the docs in the + // bucket contain the field used in the aggregation + // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066). + // Store as null, so values can be handled in the same manner downstream as other aggs + // (min, mean, max) which return null. + const medianValues = values[ML_MEDIAN_PERCENTS]; + obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null; + } else { + obj.results[dataForTime.key] = null; + } + } + }); + + return obj; + }) + ); +} + +export interface ModelPlotOutput extends ResultResponse { + results: Record; +} + +export function getModelPlotOutput( + jobId: string, + detectorIndex: number, + criteriaFields: any[], + earliestMs: number, + latestMs: number, + interval: string, + aggType?: { min: any; max: any } +): Observable { + const obj: ModelPlotOutput = { + success: true, + results: {}, + }; + + // if an aggType object has been passed in, use it. + // otherwise default to min and max aggs for the upper and lower bounds + const modelAggs = + aggType === undefined + ? { max: 'max', min: 'min' } + : { + max: aggType.max, + min: aggType.min, + }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the job ID and time range. + const mustCriteria: object[] = [ + { + term: { job_id: jobId }, + }, + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + ]; + + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, criteria => { + mustCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); + }); + + // Add criteria for the detector index. Results from jobs created before 6.1 will not + // contain a detector_index field, so use a should criteria with a 'not exists' check. + const shouldCriteria = [ + { + term: { detector_index: detectorIndex }, + }, + { + bool: { + must_not: [ + { + exists: { field: 'detector_index' }, + }, + ], + }, + }, + ]; + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:model_plot', + analyze_wildcard: true, + }, + }, + { + bool: { + must: mustCriteria, + should: shouldCriteria, + minimum_should_match: 1, + }, + }, + ], + }, + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 0, + }, + aggs: { + actual: { + avg: { + field: 'actual', + }, + }, + modelUpper: { + [modelAggs.max]: { + field: 'model_upper', + }, + }, + modelLower: { + [modelAggs.min]: { + field: 'model_lower', + }, + }, + }, + }, + }, + }, + }) + .pipe( + map(resp => { + const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); + _.each(aggregationsByTime, (dataForTime: any) => { + const time = dataForTime.key; + const modelUpper: number | undefined = _.get(dataForTime, ['modelUpper', 'value']); + const modelLower: number | undefined = _.get(dataForTime, ['modelLower', 'value']); + const actual = _.get(dataForTime, ['actual', 'value']); + + obj.results[time] = { + actual, + modelUpper: + modelUpper === undefined || isFinite(modelUpper) === false ? null : modelUpper, + modelLower: + modelLower === undefined || isFinite(modelLower) === false ? null : modelLower, + }; + }); + + return obj; + }) + ); +} + +export interface RecordsForCriteria extends ResultResponse { + records: any[]; +} + +// Queries Elasticsearch to obtain the record level results matching the given criteria, +// for the specified job(s), time range, and record score threshold. +// criteriaFields parameter must be an array, with each object in the array having 'fieldName' +// 'fieldValue' properties. +// Pass an empty array or ['*'] to search over all job IDs. +export function getRecordsForCriteria( + jobIds: string[] | undefined, + criteriaFields: CriteriaField[], + threshold: any, + earliestMs: number, + latestMs: number, + maxResults: number | undefined +): Observable { + const obj: RecordsForCriteria = { success: true, records: [] }; + + // Build the criteria to use in the bool filter part of the request. + // Add criteria for the time range, record score, plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + if (i > 0) { + jobIdFilterStr += ' OR '; + } + jobIdFilterStr += 'job_id:'; + jobIdFilterStr += jobId; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + // Add in term queries for each of the specified criteria. + _.each(criteriaFields, criteria => { + boolCriteria.push({ + term: { + [criteria.fieldName]: criteria.fieldValue, + }, + }); + }); + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + rest_total_hits_as_int: true, + size: maxResults !== undefined ? maxResults : 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }) + .pipe( + map(resp => { + if (resp.hits.total !== 0) { + _.each(resp.hits.hits, (hit: any) => { + obj.records.push(hit._source); + }); + } + return obj; + }) + ); +} + +export interface ScheduledEventsByBucket extends ResultResponse { + events: Record; +} + +// Obtains a list of scheduled events by job ID and time. +// Pass an empty array or ['*'] to search over all job IDs. +// Returned response contains a events property, which will only +// contains keys for jobs which have scheduled events for the specified time range. +export function getScheduledEventsByBucket( + jobIds: string[] | undefined, + earliestMs: number, + latestMs: number, + interval: string, + maxJobs: number, + maxEvents: number +): Observable { + const obj: ScheduledEventsByBucket = { + success: true, + events: {}, + }; + + // Build the criteria to use in the bool filter part of the request. + // Adds criteria for the time range plus any specified job IDs. + const boolCriteria: any[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + exists: { field: 'scheduled_events' }, + }, + ]; + + if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { + let jobIdFilterStr = ''; + _.each(jobIds, (jobId, i) => { + jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`; + }); + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilterStr, + }, + }); + } + + return ml + .esSearch$({ + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:bucket', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + aggs: { + jobs: { + terms: { + field: 'job_id', + min_doc_count: 1, + size: maxJobs, + }, + aggs: { + times: { + date_histogram: { + field: 'timestamp', + interval, + min_doc_count: 1, + }, + aggs: { + events: { + terms: { + field: 'scheduled_events', + size: maxEvents, + }, + }, + }, + }, + }, + }, + }, + }, + }) + .pipe( + map(resp => { + const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []); + _.each(dataByJobId, (dataForJob: any) => { + const jobId: string = dataForJob.key; + const resultsForTime: Record = {}; + const dataByTime = _.get(dataForJob, ['times', 'buckets'], []); + _.each(dataByTime, (dataForTime: any) => { + const time: string = dataForTime.key; + const events: object[] = _.get(dataForTime, ['events', 'buckets']); + resultsForTime[time] = _.map(events, 'key'); + }); + obj.events[jobId] = resultsForTime; + }); + + return obj; + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts new file mode 100644 index 0000000000000..473477a15c2f7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function getScoresByBucket( + jobIds: string[], + earliestMs: number, + latestMs: number, + interval: string | number, + maxResults: number +): Promise; +export function getTopInfluencers(): Promise; +export function getTopInfluencerValues(): Promise; +export function getOverallBucketScores( + jobIds: any, + topN: any, + earliestMs: any, + latestMs: any, + interval?: any +): Promise; +export function getInfluencerValueMaxScoreByTime(): Promise; +export function getRecordInfluencers(): Promise; +export function getRecordsForInfluencer(): Promise; +export function getRecordsForDetector(): Promise; +export function getRecords(): Promise; +export function getEventRateData( + index: string, + query: any, + timeFieldName: string, + earliestMs: number, + latestMs: number, + interval: string | number +): Promise; +export function getEventDistributionData(): Promise; +export function getRecordMaxScoreByTime(): Promise; diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service.js b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js similarity index 70% rename from x-pack/legacy/plugins/ml/public/application/services/results_service.js rename to x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js index b0840be4449bf..080ba718964c4 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/results_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/results_service.js @@ -11,17 +11,17 @@ import _ from 'lodash'; // import d3 from 'd3'; -import { ML_MEDIAN_PERCENTS } from '../../../common/util/job_utils'; -import { escapeForElasticsearchQuery } from '../util/string_utils'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { escapeForElasticsearchQuery } from '../../util/string_utils'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; -import { ml } from './ml_api_service'; +import { ml } from '../ml_api_service'; // Obtains the maximum bucket anomaly scores by job ID and time. // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a results property, with a key for job // which has results for the specified time range. -function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { +export function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { return new Promise((resolve, reject) => { const obj = { success: true, @@ -141,129 +141,13 @@ function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) { }); } -// Obtains a list of scheduled events by job ID and time. -// Pass an empty array or ['*'] to search over all job IDs. -// Returned response contains a events property, which will only -// contains keys for jobs which have scheduled events for the specified time range. -function getScheduledEventsByBucket( - jobIds, - earliestMs, - latestMs, - interval, - maxJobs, - maxEvents) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - events: {} - }; - - // Build the criteria to use in the bool filter part of the request. - // Adds criteria for the time range plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - exists: { field: 'scheduled_events' } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [{ - query_string: { - query: 'result_type:bucket', - analyze_wildcard: false - } - }, { - bool: { - must: boolCriteria - } - }] - } - }, - aggs: { - jobs: { - terms: { - field: 'job_id', - min_doc_count: 1, - size: maxJobs - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 1 - }, - aggs: { - events: { - terms: { - field: 'scheduled_events', - size: maxEvents - } - } - } - } - } - } - } - } - }) - .then((resp) => { - const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []); - _.each(dataByJobId, (dataForJob) => { - const jobId = dataForJob.key; - const resultsForTime = {}; - const dataByTime = _.get(dataForJob, ['times', 'buckets'], []); - _.each(dataByTime, (dataForTime) => { - const time = dataForTime.key; - const events = _.get(dataForTime, ['events', 'buckets']); - resultsForTime[time] = _.map(events, 'key'); - }); - obj.events[jobId] = resultsForTime; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - - // Obtains the top influencers, by maximum influencer score, for the specified index, time range and job ID(s). // Pass an empty array or ['*'] to search over all job IDs. // An optional array of influencers may be supplied, with each object in the array having 'fieldName' // and 'fieldValue' properties, to limit data to the supplied list of influencers. // Returned response contains an influencers property, with a key for each of the influencer field names, // whose value is an array of objects containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. -function getTopInfluencers( +export function getTopInfluencers( jobIds, earliestMs, latestMs, @@ -428,7 +312,7 @@ function getTopInfluencers( // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a results property, which is an array of objects // containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. -function getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestMs, maxResults) { +export function getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestMs, maxResults) { return new Promise((resolve, reject) => { const obj = { success: true, results: [] }; @@ -528,7 +412,7 @@ function getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestM // Obtains the overall bucket scores for the specified job ID(s). // Pass ['*'] to search over all job IDs. // Returned response contains a results property as an object of max score by time. -function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { +export function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { return new Promise((resolve, reject) => { const obj = { success: true, results: {} }; @@ -561,7 +445,7 @@ function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) { // values (pass an empty array to search over all field values). // Returned response contains a results property with influencer field values keyed // against max score by time. -function getInfluencerValueMaxScoreByTime( +export function getInfluencerValueMaxScoreByTime( jobIds, influencerFieldName, influencerFieldValues, @@ -720,7 +604,7 @@ function getInfluencerValueMaxScoreByTime( // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a records property, with each record containing // only the fields job_id, detector_index, record_score and influencers. -function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResults) { +export function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResults) { return new Promise((resolve, reject) => { const obj = { success: true, records: [] }; @@ -826,7 +710,7 @@ function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResult // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query, // so this returns record level results which have at least one of the influencers. // Pass an empty array or ['*'] to search over all job IDs. -function getRecordsForInfluencer(jobIds, influencers, threshold, earliestMs, latestMs, maxResults, influencersFilterQuery) { +export function getRecordsForInfluencer(jobIds, influencers, threshold, earliestMs, latestMs, maxResults, influencersFilterQuery) { return new Promise((resolve, reject) => { const obj = { success: true, records: [] }; @@ -949,7 +833,7 @@ function getRecordsForInfluencer(jobIds, influencers, threshold, earliestMs, lat // Queries Elasticsearch to obtain the record level results for the specified job and detector, // time range, record score threshold, and whether to only return results containing influencers. // An additional, optional influencer field name and value may also be provided. -function getRecordsForDetector( +export function getRecordsForDetector( jobId, detectorIndex, checkForInfluencers, @@ -1076,270 +960,17 @@ function getRecordsForDetector( // and record score threshold. // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a records property, which is an array of the matching results. -function getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { +export function getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) { return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults); } -// Queries Elasticsearch to obtain the record level results matching the given criteria, -// for the specified job(s), time range, and record score threshold. -// criteriaFields parameter must be an array, with each object in the array having 'fieldName' -// 'fieldValue' properties. -// Pass an empty array or ['*'] to search over all job IDs. -function getRecordsForCriteria(jobIds, criteriaFields, threshold, earliestMs, latestMs, maxResults) { - return new Promise((resolve, reject) => { - const obj = { success: true, records: [] }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, record score, plus any specified job IDs. - const boolCriteria = [ - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }, - { - range: { - record_score: { - gte: threshold, - } - } - } - ]; - - if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) { - let jobIdFilterStr = ''; - _.each(jobIds, (jobId, i) => { - if (i > 0) { - jobIdFilterStr += ' OR '; - } - jobIdFilterStr += 'job_id:'; - jobIdFilterStr += jobId; - }); - boolCriteria.push({ - query_string: { - analyze_wildcard: false, - query: jobIdFilterStr - } - }); - } - - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - boolCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); - }); - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - rest_total_hits_as_int: true, - size: maxResults !== undefined ? maxResults : 100, - body: { - query: { - bool: { - filter: [ - { - query_string: { - query: 'result_type:record', - analyze_wildcard: false - } - }, - { - bool: { - must: boolCriteria - } - } - ] - } - }, - sort: [ - { record_score: { order: 'desc' } } - ], - } - }) - .then((resp) => { - if (resp.hits.total !== 0) { - _.each(resp.hits.hits, (hit) => { - obj.records.push(hit._source); - }); - } - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - - -// Queries Elasticsearch to obtain metric aggregation results. -// index can be a String, or String[], of index names to search. -// entityFields parameter must be an array, with each object in the array having 'fieldName' -// and 'fieldValue' properties. -// Extra query object can be supplied, or pass null if no additional query -// to that built from the supplied entity fields. -// Returned response contains a results property containing the requested aggregation. -function getMetricData( - index, - entityFields, - query, - metricFunction, // ES aggregation name - metricFieldName, - timeFieldName, - earliestMs, - latestMs, - interval) { - return new Promise((resolve, reject) => { - const obj = { success: true, results: {} }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the time range, entity fields, - // plus any additional supplied query. - const mustCriteria = []; - const shouldCriteria = []; - - mustCriteria.push({ - range: { - [timeFieldName]: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - }); - - if (query) { - mustCriteria.push(query); - } - - _.each(entityFields, (entity) => { - if (entity.fieldValue.length !== 0) { - mustCriteria.push({ - term: { - [entity.fieldName]: entity.fieldValue - } - }); - - } else { - // Add special handling for blank entity field values, checking for either - // an empty string or the field not existing. - shouldCriteria.push({ - bool: { - must: [ - { - term: { - [entity.fieldName]: '' - } - } - ] - } - }); - shouldCriteria.push({ - bool: { - must_not: [ - { - exists: { field: entity.fieldName } - } - ] - } - }); - } - - }); - - const body = { - query: { - bool: { - must: mustCriteria - } - }, - size: 0, - _source: { - excludes: [] - }, - aggs: { - byTime: { - date_histogram: { - field: timeFieldName, - interval: interval, - min_doc_count: 0 - } - - } - } - }; - - if (shouldCriteria.length > 0) { - body.query.bool.should = shouldCriteria; - body.query.bool.minimum_should_match = shouldCriteria.length / 2; - } - - if (metricFieldName !== undefined && metricFieldName !== '') { - body.aggs.byTime.aggs = {}; - - const metricAgg = { - [metricFunction]: { - field: metricFieldName - } - }; - - if (metricFunction === 'percentiles') { - metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; - } - body.aggs.byTime.aggs.metric = metricAgg; - } - - ml.esSearch({ - index, - body - }) - .then((resp) => { - const dataByTime = _.get(resp, ['aggregations', 'byTime', 'buckets'], []); - _.each(dataByTime, (dataForTime) => { - if (metricFunction === 'count') { - obj.results[dataForTime.key] = dataForTime.doc_count; - } else { - const value = _.get(dataForTime, ['metric', 'value']); - const values = _.get(dataForTime, ['metric', 'values']); - if (dataForTime.doc_count === 0) { - obj.results[dataForTime.key] = null; - } else if (value !== undefined) { - obj.results[dataForTime.key] = value; - } else if (values !== undefined) { - // Percentiles agg currently returns NaN rather than null when none of the docs in the - // bucket contain the field used in the aggregation - // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066). - // Store as null, so values can be handled in the same manner downstream as other aggs - // (min, mean, max) which return null. - const medianValues = values[ML_MEDIAN_PERCENTS]; - obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null; - } else { - obj.results[dataForTime.key] = null; - } - } - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - // Queries Elasticsearch to obtain event rate data i.e. the count // of documents over time. // index can be a String, or String[], of index names to search. // Extra query object can be supplied, or pass null if no additional query. // Returned response contains a results property, which is an object // of document counts against time (epoch millis). -function getEventRateData( +export function getEventRateData( index, query, timeFieldName, @@ -1420,7 +1051,7 @@ const SAMPLER_TOP_TERMS_SHARD_SIZE = 20000; const ENTITY_AGGREGATION_SIZE = 10; const AGGREGATION_MIN_DOC_COUNT = 1; const CARDINALITY_PRECISION_THRESHOLD = 100; -function getEventDistributionData( +export function getEventDistributionData( index, splitField, filterField = null, @@ -1583,155 +1214,11 @@ function getEventDistributionData( }); } -function getModelPlotOutput( - jobId, - detectorIndex, - criteriaFields, - earliestMs, - latestMs, - interval, - aggType) { - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; - - // if an aggType object has been passed in, use it. - // otherwise default to min and max aggs for the upper and lower bounds - const modelAggs = (aggType === undefined) ? - { max: 'max', min: 'min' } : - { - max: aggType.max, - min: aggType.min - }; - - // Build the criteria to use in the bool filter part of the request. - // Add criteria for the job ID and time range. - const mustCriteria = [ - { - term: { job_id: jobId } - }, - { - range: { - timestamp: { - gte: earliestMs, - lte: latestMs, - format: 'epoch_millis' - } - } - } - ]; - - // Add in term queries for each of the specified criteria. - _.each(criteriaFields, (criteria) => { - mustCriteria.push({ - term: { - [criteria.fieldName]: criteria.fieldValue - } - }); - }); - - // Add criteria for the detector index. Results from jobs created before 6.1 will not - // contain a detector_index field, so use a should criteria with a 'not exists' check. - const shouldCriteria = [ - { - term: { detector_index: detectorIndex } - }, - { - bool: { - must_not: [ - { - exists: { field: 'detector_index' } - } - ] - } - } - ]; - - ml.esSearch({ - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [{ - query_string: { - query: 'result_type:model_plot', - analyze_wildcard: true - } - }, { - bool: { - must: mustCriteria, - should: shouldCriteria, - minimum_should_match: 1 - } - }] - } - }, - aggs: { - times: { - date_histogram: { - field: 'timestamp', - interval: interval, - min_doc_count: 0 - }, - aggs: { - actual: { - avg: { - field: 'actual' - } - }, - modelUpper: { - [modelAggs.max]: { - field: 'model_upper' - } - }, - modelLower: { - [modelAggs.min]: { - field: 'model_lower' - } - } - } - } - } - } - }) - .then((resp) => { - const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []); - _.each(aggregationsByTime, (dataForTime) => { - const time = dataForTime.key; - let modelUpper = _.get(dataForTime, ['modelUpper', 'value']); - let modelLower = _.get(dataForTime, ['modelLower', 'value']); - const actual = _.get(dataForTime, ['actual', 'value']); - - if (modelUpper === undefined || isFinite(modelUpper) === false) { - modelUpper = null; - } - if (modelLower === undefined || isFinite(modelLower) === false) { - modelLower = null; - } - - obj.results[time] = { - actual, - modelUpper, - modelLower - }; - }); - - resolve(obj); - }) - .catch((resp) => { - reject(resp); - }); - }); -} - // Queries Elasticsearch to obtain the max record score over time for the specified job, // criteria, time range, and aggregation interval. // criteriaFields parameter must be an array, with each object in the array having 'fieldName' // 'fieldValue' properties. -function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) { +export function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) { return new Promise((resolve, reject) => { const obj = { success: true, @@ -1840,22 +1327,3 @@ function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, in }); }); } - -export const mlResultsService = { - getScoresByBucket, - getScheduledEventsByBucket, - getTopInfluencers, - getTopInfluencerValues, - getOverallBucketScores, - getInfluencerValueMaxScoreByTime, - getRecordInfluencers, - getRecordsForInfluencer, - getRecordsForDetector, - getRecords, - getRecordsForCriteria, - getMetricData, - getEventRateData, - getEventDistributionData, - getModelPlotOutput, - getRecordMaxScoreByTime -}; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 5d621a51b710e..eb4dfae3f5ff3 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -1081,10 +1081,6 @@ const TimeseriesChartIntl = injectI18n(class TimeseriesChart extends React.Compo const that = this; function brushed() { - if (that.props.skipRefresh) { - return; - } - const isEmpty = brush.empty(); const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js index 946312d08e9ce..5aa6cfe8835ad 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import './timeseriesexplorer_directive.js'; -import './timeseriesexplorer_route.js'; -import './timeseries_search_service.js'; +import './timeseriesexplorer_directive'; +import './timeseriesexplorer_route'; +import './timeseries_search_service'; import '../components/job_selector'; import '../components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts similarity index 69% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 5cbbd530c96f1..65bcc9d355fd6 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -4,44 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ - - import _ from 'lodash'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { ml } from '../services/ml_api_service'; import { isModelPlotEnabled } from '../../../common/util/job_utils'; +// @ts-ignore import { buildConfigFromDetector } from '../util/chart_config_builder'; import { mlResultsService } from '../services/results_service'; - -function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, interval) { +import { ModelPlotOutput } from '../services/results_service/result_service_rx'; +import { Job } from '../jobs/new_job/common/job_creator/configs'; + +function getMetricData( + job: Job, + detectorIndex: number, + entityFields: object[], + earliestMs: number, + latestMs: number, + interval: string +): Observable { if (isModelPlotEnabled(job, detectorIndex, entityFields)) { // Extract the partition, by, over fields on which to filter. const criteriaFields = []; const detector = job.analysis_config.detectors[detectorIndex]; if (_.has(detector, 'partition_field_name')) { - const partitionEntity = _.find(entityFields, { 'fieldName': detector.partition_field_name }); + const partitionEntity: any = _.find(entityFields, { + fieldName: detector.partition_field_name, + }); if (partitionEntity !== undefined) { criteriaFields.push( { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, - { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue }); + { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue } + ); } } if (_.has(detector, 'over_field_name')) { - const overEntity = _.find(entityFields, { 'fieldName': detector.over_field_name }); + const overEntity: any = _.find(entityFields, { fieldName: detector.over_field_name }); if (overEntity !== undefined) { criteriaFields.push( { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, - { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue }); + { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue } + ); } } if (_.has(detector, 'by_field_name')) { - const byEntity = _.find(entityFields, { 'fieldName': detector.by_field_name }); + const byEntity: any = _.find(entityFields, { fieldName: detector.by_field_name }); if (byEntity !== undefined) { criteriaFields.push( { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, - { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue }); + { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue } + ); } } @@ -54,15 +69,15 @@ function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, i interval ); } else { - return new Promise((resolve, reject) => { - const obj = { - success: true, - results: {} - }; + const obj: ModelPlotOutput = { + success: true, + results: {}, + }; - const chartConfig = buildConfigFromDetector(job, detectorIndex); + const chartConfig = buildConfigFromDetector(job, detectorIndex); - mlResultsService.getMetricData( + return mlResultsService + .getMetricData( chartConfig.datafeedConfig.indices, entityFields, chartConfig.datafeedConfig.query, @@ -73,20 +88,17 @@ function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, i latestMs, interval ) - .then((resp) => { + .pipe( + map(resp => { _.each(resp.results, (value, time) => { + // @ts-ignore obj.results[time] = { - 'actual': value + actual: value, }; }); - - resolve(obj); + return obj; }) - .catch((resp) => { - reject(resp); - }); - - }); + ); } } @@ -94,9 +106,18 @@ function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, i // in the title area of the time series chart. // Queries Elasticsearch if necessary to obtain the distinct count of entities // for which data is being plotted. -function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs) { +function getChartDetails( + job: Job, + detectorIndex: number, + entityFields: any[], + earliestMs: number, + latestMs: number +) { return new Promise((resolve, reject) => { - const obj = { success: true, results: { functionLabel: '', entityData: { entities: [] } } }; + const obj: any = { + success: true, + results: { functionLabel: '', entityData: { entities: [] } }, + }; const chartConfig = buildConfigFromDetector(job, detectorIndex); let functionLabel = chartConfig.metricFunction; @@ -106,7 +127,7 @@ function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs) } obj.results.functionLabel = functionLabel; - const blankEntityFields = _.filter(entityFields, (entity) => { + const blankEntityFields = _.filter(entityFields, entity => { return entity.fieldValue.length === 0; }); @@ -124,29 +145,28 @@ function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs) query: chartConfig.datafeedConfig.query, timeFieldName: chartConfig.timeField, earliestMs, - latestMs + latestMs, }) - .then((results) => { - _.each(blankEntityFields, (field) => { + .then((results: any) => { + _.each(blankEntityFields, field => { // results will not contain keys for non-aggregatable fields, // so store as 0 to indicate over all field values. obj.results.entityData.entities.push({ fieldName: field.fieldName, - cardinality: _.get(results, field.fieldName, 0) + cardinality: _.get(results, field.fieldName, 0), }); }); resolve(obj); }) - .catch((resp) => { + .catch((resp: any) => { reject(resp); }); } - }); } export const mlTimeSeriesSearchService = { getMetricData, - getChartDetails + getChartDetails, }; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 8492ab11474f5..02e29c1117ffc 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -8,9 +8,10 @@ * React component for rendering Single Metric Viewer. */ -import { chain, difference, each, find, filter, first, get, has, isEqual, without } from 'lodash'; +import { chain, difference, each, find, first, get, has, isEqual, without } from 'lodash'; import moment from 'moment-timezone'; -import { Subscription } from 'rxjs'; +import { Subject, Subscription, forkJoin } from 'rxjs'; +import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import PropTypes from 'prop-types'; import React, { createRef, Fragment } from 'react'; @@ -78,11 +79,10 @@ import { calculateInitialFocusRange, createTimeSeriesJobData, getAutoZoomDuration, - getFocusData, processForecastResults, processMetricPlotResults, processRecordScoreResults, -} from './timeseriesexplorer_utils'; + getFocusData } from './timeseriesexplorer_utils'; const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); @@ -179,6 +179,11 @@ export class TimeSeriesExplorer extends React.Component { }); } + /** + * Subject for listening brush time range selection. + */ + contextChart$ = new Subject(); + detectorIndexChangeHandler = (e) => { const id = e.target.value; if (id !== undefined) { @@ -252,109 +257,85 @@ export class TimeSeriesExplorer extends React.Component { } contextChartSelectedInitCallDone = false; - contextChartSelected = (selection) => { - const { appStateHandler } = this.props; + /** + * Gets default range from component state. + */ + getDefaultRangeFromState() { const { autoZoomDuration, contextAggregationInterval, contextChartData, contextForecastData, - focusChartData, - jobs, - selectedJob, - zoomFromFocusLoaded, - zoomToFocusLoaded, } = this.state; - - if ((contextChartData === undefined || contextChartData.length === 0) && - (contextForecastData === undefined || contextForecastData.length === 0)) { - return; - } - - const stateUpdate = {}; - - const defaultRange = calculateDefaultFocusRange( + return calculateDefaultFocusRange( autoZoomDuration, contextAggregationInterval, contextChartData, contextForecastData, ); + } - if ((selection.from.getTime() !== defaultRange[0].getTime() || selection.to.getTime() !== defaultRange[1].getTime()) && - (isNaN(Date.parse(selection.from)) === false && isNaN(Date.parse(selection.to)) === false)) { - const zoomState = { from: selection.from.toISOString(), to: selection.to.toISOString() }; - appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState); - } else { - appStateHandler(APP_STATE_ACTION.UNSET_ZOOM); - } + getFocusAggregationInterval(selection) { + const { + jobs, + selectedJob, + } = this.state; - this.setState({ - zoomFrom: selection.from, - zoomTo: selection.to, - }); + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; - if ( - (this.contextChartSelectedInitCallDone === false && focusChartData === undefined) || - (zoomFromFocusLoaded.getTime() !== selection.from.getTime()) || - (zoomToFocusLoaded.getTime() !== selection.to.getTime()) - ) { - this.contextChartSelectedInitCallDone = true; + return calculateAggregationInterval( + bounds, + CHARTS_POINT_TARGET, + jobs, + selectedJob, + ); + } - // Calculate the aggregation interval for the focus chart. - const bounds = { min: moment(selection.from), max: moment(selection.to) }; - const focusAggregationInterval = calculateAggregationInterval( - bounds, - CHARTS_POINT_TARGET, - jobs, - selectedJob, - ); - stateUpdate.focusAggregationInterval = focusAggregationInterval; + /** + * Gets focus data for the current component state/ + */ + getFocusData(selection) { + const { + detectorId, + entities, + modelPlotEnabled, + selectedJob, + } = this.state; - // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. - // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected - // to some extent with all detector functions if not searching complete buckets. - const searchBounds = getBoundsRoundedToInterval(bounds, focusAggregationInterval, false); + const { appStateHandler } = this.props; - const { - detectorId, - entities, - modelPlotEnabled, - } = this.state; - - this.setState({ - loading: true, - fullRefresh: false, - zoomFrom: selection.from, - zoomTo: selection.to, - }); + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; - getFocusData( - this._criteriaFields, - +detectorId, - focusAggregationInterval, - appStateHandler(APP_STATE_ACTION.GET_FORECAST_ID), - modelPlotEnabled, - filter(entities, entity => entity.fieldValue.length > 0), - searchBounds, - selectedJob, - TIME_FIELD_NAME, - ).then((refreshFocusData) => { - // All the data is ready now for a state update. - this.setState({ - ...stateUpdate, - ...refreshFocusData, - loading: false, - showModelBoundsCheckbox: (modelPlotEnabled === true) && (refreshFocusData.focusChartData.length > 0), - zoomFromFocusLoaded: selection.from, - zoomToFocusLoaded: selection.to, - }); - }); + const focusAggregationInterval = this.getFocusAggregationInterval(selection); - // Load the data for the anomalies table. - this.loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf()); - } + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = getBoundsRoundedToInterval( + bounds, + focusAggregationInterval, + false + ); + + return getFocusData( + this._criteriaFields, + +detectorId, + focusAggregationInterval, + appStateHandler(APP_STATE_ACTION.GET_FORECAST_ID), + modelPlotEnabled, + entities.filter(entity => entity.fieldValue.length > 0), + searchBounds, + selectedJob, + TIME_FIELD_NAME + ); + } + + contextChartSelected = (selection) => { + this.contextChart$.next(selection); } entityFieldValueChanged = (entity, fieldValue) => { @@ -380,7 +361,7 @@ export class TimeSeriesExplorer extends React.Component { const { dateFormatTz } = this.props; const { selectedJob } = this.state; - ml.results.getAnomaliesTableData( + return ml.results.getAnomaliesTableData( [selectedJob.job_id], this._criteriaFields, [], @@ -390,43 +371,43 @@ export class TimeSeriesExplorer extends React.Component { latestMs, dateFormatTz, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE - ).then((resp) => { - const anomalies = resp.anomalies; - const detectorsByJob = mlJobService.detectorsByJob; - anomalies.forEach((anomaly) => { - // Add a detector property to each anomaly. - // Default to functionDescription if no description available. - // TODO - when job_service is moved server_side, move this to server endpoint. - const jobId = anomaly.jobId; - const detector = get(detectorsByJob, [jobId, anomaly.detectorIndex]); - anomaly.detector = get(detector, - ['detector_description'], - anomaly.source.function_description); - - // For detectors with rules, add a property with the rule count. - const customRules = detector.custom_rules; - if (customRules !== undefined) { - anomaly.rulesLength = customRules.length; - } + ).pipe( + map(resp => { + const anomalies = resp.anomalies; + const detectorsByJob = mlJobService.detectorsByJob; + anomalies.forEach((anomaly) => { + // Add a detector property to each anomaly. + // Default to functionDescription if no description available. + // TODO - when job_service is moved server_side, move this to server endpoint. + const jobId = anomaly.jobId; + const detector = get(detectorsByJob, [jobId, anomaly.detectorIndex]); + anomaly.detector = get(detector, + ['detector_description'], + anomaly.source.function_description); + + // For detectors with rules, add a property with the rule count. + const customRules = detector.custom_rules; + if (customRules !== undefined) { + anomaly.rulesLength = customRules.length; + } - // Add properties used for building the links menu. - // TODO - when job_service is moved server_side, move this to server endpoint. - if (has(mlJobService.customUrlsByJob, jobId)) { - anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; - } - }); + // Add properties used for building the links menu. + // TODO - when job_service is moved server_side, move this to server endpoint. + if (has(mlJobService.customUrlsByJob, jobId)) { + anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; + } + }); - this.setState({ - tableData: { - anomalies, - interval: resp.interval, - examplesByJobId: resp.examplesByJobId, - showViewSeriesLink: false - } - }); - }).catch((resp) => { - console.log('Time series explorer - error loading data for anomalies table:', resp); - }); + return { + tableData: { + anomalies, + interval: resp.interval, + examplesByJobId: resp.examplesByJobId, + showViewSeriesLink: false + } + }; + }) + ); } loadEntityValues = (callback = () => {}) => { @@ -445,6 +426,7 @@ export class TimeSeriesExplorer extends React.Component { bounds.min.valueOf(), bounds.max.valueOf(), ANOMALIES_TABLE_DEFAULT_QUERY_SIZE) + .toPromise() .then((resp) => { if (resp.records && resp.records.length > 0) { const firstRec = resp.records[0]; @@ -604,7 +586,7 @@ export class TimeSeriesExplorer extends React.Component { } }; - const nonBlankEntities = filter(currentEntities, (entity) => { return entity.fieldValue.length > 0; }); + const nonBlankEntities = currentEntities.filter((entity) => { return entity.fieldValue.length > 0; }); if (modelPlotEnabled === false && isSourceDataChartableForDetector(selectedJob, detectorIndex) === false && @@ -646,7 +628,7 @@ export class TimeSeriesExplorer extends React.Component { searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.expression - ).then((resp) => { + ).toPromise().then((resp) => { const fullRangeChartData = processMetricPlotResults(resp.results, modelPlotEnabled); stateUpdate.contextChartData = fullRangeChartData; finish(counter); @@ -702,7 +684,8 @@ export class TimeSeriesExplorer extends React.Component { searchBounds.min.valueOf(), searchBounds.max.valueOf(), stateUpdate.contextAggregationInterval.expression, - aggType) + aggType + ).toPromise() .then((resp) => { stateUpdate.contextForecastData = processForecastResults(resp.results); finish(counter); @@ -762,7 +745,7 @@ export class TimeSeriesExplorer extends React.Component { */ updateCriteriaFields(detectorIndex, entities) { // Only filter on the entity if the field has a value. - const nonBlankEntities = filter(entities, (entity) => { return entity.fieldValue.length > 0; }); + const nonBlankEntities = entities.filter(entity => entity.fieldValue.length > 0); this._criteriaFields = [ { fieldName: 'detector_index', @@ -868,7 +851,8 @@ export class TimeSeriesExplorer extends React.Component { const tableControlsListener = () => { const { zoomFrom, zoomTo } = this.state; if (zoomFrom !== undefined && zoomTo !== undefined) { - this.loadAnomaliesTableData(zoomFrom.getTime(), zoomTo.getTime()); + this.loadAnomaliesTableData(zoomFrom.getTime(), zoomTo.getTime()).subscribe(res => + this.setState(res)); } }; @@ -978,6 +962,97 @@ export class TimeSeriesExplorer extends React.Component { this.resizeHandler(); }); this.resizeHandler(); + + // Listen for context chart updates. + this.subscriptions.add(this.contextChart$ + .pipe( + tap(selection => { + this.setState({ + zoomFrom: selection.from, + zoomTo: selection.to, + }); + }), + debounceTime(500), + tap((selection) => { + const { + contextChartData, + contextForecastData, + focusChartData, + zoomFromFocusLoaded, + zoomToFocusLoaded, + } = this.state; + + if ((contextChartData === undefined || contextChartData.length === 0) && + (contextForecastData === undefined || contextForecastData.length === 0)) { + return; + } + + const defaultRange = this.getDefaultRangeFromState(); + + if ((selection.from.getTime() !== defaultRange[0].getTime() || selection.to.getTime() !== defaultRange[1].getTime()) && + (isNaN(Date.parse(selection.from)) === false && isNaN(Date.parse(selection.to)) === false)) { + const zoomState = { from: selection.from.toISOString(), to: selection.to.toISOString() }; + appStateHandler(APP_STATE_ACTION.SET_ZOOM, zoomState); + } else { + appStateHandler(APP_STATE_ACTION.UNSET_ZOOM); + } + + if ( + (this.contextChartSelectedInitCallDone === false && focusChartData === undefined) || + (zoomFromFocusLoaded.getTime() !== selection.from.getTime()) || + (zoomToFocusLoaded.getTime() !== selection.to.getTime()) + ) { + this.contextChartSelectedInitCallDone = true; + + this.setState({ + loading: true, + fullRefresh: false, + }); + } + }), + switchMap(selection => { + const { + jobs, + selectedJob + } = this.state; + + // Calculate the aggregation interval for the focus chart. + const bounds = { min: moment(selection.from), max: moment(selection.to) }; + const focusAggregationInterval = calculateAggregationInterval( + bounds, + CHARTS_POINT_TARGET, + jobs, + selectedJob, + ); + + // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. + // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected + // to some extent with all detector functions if not searching complete buckets. + const searchBounds = getBoundsRoundedToInterval(bounds, focusAggregationInterval, false); + return forkJoin([ + this.getFocusData(selection), + // Load the data for the anomalies table. + this.loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf()) + ]); + }), + withLatestFrom(this.contextChart$) + ) + .subscribe(([[refreshFocusData, tableData], selection]) => { + const { + modelPlotEnabled, + } = this.state; + + // All the data is ready now for a state update. + this.setState({ + focusAggregationInterval: this.getFocusAggregationInterval({ from: selection.from, to: selection.to }), + loading: false, + showModelBoundsCheckbox: modelPlotEnabled && (refreshFocusData.focusChartData.length > 0), + zoomFromFocusLoaded: selection.from, + zoomToFocusLoaded: selection.to, + ...refreshFocusData, + ...tableData + }); + })); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts similarity index 99% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts index 52590bb6824c1..29a5facf64c0f 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts @@ -8,7 +8,6 @@ * Contains values for ML time series explorer. */ - export const APP_STATE_ACTION = { CLEAR: 'CLEAR', GET_DETECTOR_INDEX: 'GET_DETECTOR_INDEX', diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts new file mode 100644 index 0000000000000..03fe718de9bed --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { forkJoin, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import chrome from 'ui/chrome'; +import { ml } from '../../services/ml_api_service'; +import { + ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, +} from '../../../../common/constants/search'; +import { mlTimeSeriesSearchService } from '../timeseries_search_service'; +import { mlResultsService, CriteriaField } from '../../services/results_service'; +import { Job } from '../../jobs/new_job/common/job_creator/configs'; +import { MAX_SCHEDULED_EVENTS, TIME_FIELD_NAME } from '../timeseriesexplorer_constants'; +import { + processDataForFocusAnomalies, + processForecastResults, + processMetricPlotResults, + processScheduledEventsForChart, +} from './timeseriesexplorer_utils'; +import { mlForecastService } from '../../services/forecast_service'; +import { mlFunctionToESAggregation } from '../../../../common/util/job_utils'; +import { Annotation } from '../../../../common/types/annotations'; + +const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); + +export interface Interval { + asMilliseconds: () => number; + expression: string; +} + +export interface FocusData { + focusChartData: any; + anomalyRecords: any; + scheduledEvents: any; + showForecastCheckbox?: any; + focusAnnotationData?: any; + focusForecastData?: any; +} + +export function getFocusData( + criteriaFields: CriteriaField[], + detectorIndex: number, + focusAggregationInterval: Interval, + forecastId: string, + modelPlotEnabled: boolean, + nonBlankEntities: any[], + searchBounds: any, + selectedJob: Job +): Observable { + return forkJoin([ + // Query 1 - load metric data across selected time range. + mlTimeSeriesSearchService.getMetricData( + selectedJob, + detectorIndex, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression + ), + // Query 2 - load all the records across selected time range for the chart anomaly markers. + mlResultsService.getRecordsForCriteria( + [selectedJob.job_id], + criteriaFields, + 0, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + ANOMALIES_TABLE_DEFAULT_QUERY_SIZE + ), + // Query 3 - load any scheduled events for the selected job. + mlResultsService.getScheduledEventsByBucket( + [selectedJob.job_id], + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression, + 1, + MAX_SCHEDULED_EVENTS + ), + // Query 4 - load any annotations for the selected job. + mlAnnotationsEnabled + ? ml.annotations + .getAnnotations({ + jobIds: [selectedJob.job_id], + earliestMs: searchBounds.min.valueOf(), + latestMs: searchBounds.max.valueOf(), + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + }) + .pipe( + catchError(() => { + // silent fail + return of({ annotations: {} as Record }); + }) + ) + : of(null), + // Plus query for forecast data if there is a forecastId stored in the appState. + forecastId !== undefined + ? (() => { + let aggType; + const detector = selectedJob.analysis_config.detectors[detectorIndex]; + const esAgg = mlFunctionToESAggregation(detector.function); + if (!modelPlotEnabled && (esAgg === 'sum' || esAgg === 'count')) { + aggType = { avg: 'sum', max: 'sum', min: 'sum' }; + } + return mlForecastService.getForecastData( + selectedJob, + detectorIndex, + forecastId, + nonBlankEntities, + searchBounds.min.valueOf(), + searchBounds.max.valueOf(), + focusAggregationInterval.expression, + aggType + ); + })() + : of(null), + ]).pipe( + map(([metricData, recordsForCriteria, scheduledEventsByBucket, annotations, forecastData]) => { + // Sort in descending time order before storing in scope. + const anomalyRecords = recordsForCriteria.records + .sort((a, b) => a[TIME_FIELD_NAME] - b[TIME_FIELD_NAME]) + .reverse(); + + const scheduledEvents = scheduledEventsByBucket.events[selectedJob.job_id]; + + let focusChartData = processMetricPlotResults(metricData.results, modelPlotEnabled); + // Tell the results container directives to render the focus chart. + focusChartData = processDataForFocusAnomalies( + focusChartData, + anomalyRecords, + focusAggregationInterval, + modelPlotEnabled + ); + focusChartData = processScheduledEventsForChart(focusChartData, scheduledEvents); + + const refreshFocusData: FocusData = { + scheduledEvents, + anomalyRecords, + focusChartData, + }; + + if (annotations) { + refreshFocusData.focusAnnotationData = (annotations.annotations[selectedJob.job_id] ?? []) + .sort((a, b) => { + return a.timestamp - b.timestamp; + }) + .map((d, i) => { + d.key = String.fromCharCode(65 + i); + return d; + }); + } + + if (forecastData) { + refreshFocusData.focusForecastData = processForecastResults(forecastData.results); + refreshFocusData.showForecastCheckbox = refreshFocusData.focusForecastData.length > 0; + } + + return refreshFocusData; + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts new file mode 100644 index 0000000000000..578dbdf1277a0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getFocusData } from './get_focus_data'; +export * from './timeseriesexplorer_utils'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts new file mode 100644 index 0000000000000..1528ac887ad76 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.d.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function createTimeSeriesJobData(jobs: any): any; + +export function processMetricPlotResults(metricPlotData: any, modelPlotEnabled: any): any; + +export function processForecastResults(forecastData: any): any; + +export function processRecordScoreResults(scoreData: any): any; + +export function processDataForFocusAnomalies( + chartData: any, + anomalyRecords: any, + aggregationInterval: any, + modelPlotEnabled: any +): any; + +export function processScheduledEventsForChart(chartData: any, scheduledEvents: any): any; + +export function findNearestChartPointToTime(chartData: any, time: any): any; + +export function findChartPointForAnomalyTime( + chartData: any, + anomalyTime: any, + aggregationInterval: any +): any; + +export function calculateAggregationInterval( + bounds: any, + bucketsTarget: any, + jobs: any, + selectedJob: any +): any; + +export function calculateDefaultFocusRange( + autoZoomDuration: any, + contextAggregationInterval: any, + contextChartData: any, + contextForecastData: any +): any; + +export function calculateInitialFocusRange( + zoomState: any, + contextAggregationInterval: any, + timefilter: any +): any; + +export function getAutoZoomDuration(jobs: any, selectedJob: any): any; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js similarity index 70% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js index 61f5a76a5877e..b9c9ed87ddbc7 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js @@ -15,31 +15,17 @@ import _ from 'lodash'; import moment from 'moment-timezone'; -import { - ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE -} from '../../../common/constants/search'; import { isTimeSeriesViewJob, - mlFunctionToESAggregation, -} from '../../../common/util/job_utils'; -import { parseInterval } from '../../../common/util/parse_interval'; - -import { ml } from '../services/ml_api_service'; -import { mlForecastService } from '../services/forecast_service'; -import { mlResultsService } from '../services/results_service'; -import { TimeBuckets, getBoundsRoundedToInterval } from '../util/time_buckets'; +} from '../../../../common/util/job_utils'; +import { parseInterval } from '../../../../common/util/parse_interval'; -import { mlTimeSeriesSearchService } from './timeseries_search_service'; +import { TimeBuckets, getBoundsRoundedToInterval } from '../../util/time_buckets'; import { CHARTS_POINT_TARGET, - MAX_SCHEDULED_EVENTS, TIME_FIELD_NAME, -} from './timeseriesexplorer_constants'; - -import chrome from 'ui/chrome'; -const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); +} from '../timeseriesexplorer_constants'; // create new job objects based on standard job config objects // new job objects just contain job id, bucket span in seconds and a selected flag. @@ -301,154 +287,6 @@ export function findChartPointForAnomalyTime(chartData, anomalyTime, aggregation return chartPoint; } -export const getFocusData = function ( - criteriaFields, - detectorIndex, - focusAggregationInterval, - forecastId, - modelPlotEnabled, - nonBlankEntities, - searchBounds, - selectedJob, -) { - return new Promise((resolve, reject) => { - // Counter to keep track of the queries to populate the chart. - let awaitingCount = 4; - - // This object is used to store the results of individual remote requests - // before we transform it into the final data and apply it to $scope. Otherwise - // we might trigger multiple $digest cycles and depending on how deep $watches - // listen for changes we could miss updates. - const refreshFocusData = {}; - - // finish() function, called after each data set has been loaded and processed. - // The last one to call it will trigger the page render. - function finish() { - awaitingCount--; - if (awaitingCount === 0) { - // Tell the results container directives to render the focus chart. - refreshFocusData.focusChartData = processDataForFocusAnomalies( - refreshFocusData.focusChartData, - refreshFocusData.anomalyRecords, - focusAggregationInterval, - modelPlotEnabled, - ); - - refreshFocusData.focusChartData = processScheduledEventsForChart( - refreshFocusData.focusChartData, - refreshFocusData.scheduledEvents); - - resolve(refreshFocusData); - } - } - - // Query 1 - load metric data across selected time range. - mlTimeSeriesSearchService.getMetricData( - selectedJob, - detectorIndex, - nonBlankEntities, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression - ).then((resp) => { - refreshFocusData.focusChartData = processMetricPlotResults(resp.results, modelPlotEnabled); - finish(); - }).catch((resp) => { - console.log('Time series explorer - error getting metric data from elasticsearch:', resp); - reject(); - }); - - // Query 2 - load all the records across selected time range for the chart anomaly markers. - mlResultsService.getRecordsForCriteria( - [selectedJob.job_id], - criteriaFields, - 0, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - ANOMALIES_TABLE_DEFAULT_QUERY_SIZE - ).then((resp) => { - // Sort in descending time order before storing in scope. - refreshFocusData.anomalyRecords = _.chain(resp.records) - .sortBy(record => record[TIME_FIELD_NAME]) - .reverse() - .value(); - finish(); - }); - - // Query 3 - load any scheduled events for the selected job. - mlResultsService.getScheduledEventsByBucket( - [selectedJob.job_id], - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression, - 1, - MAX_SCHEDULED_EVENTS - ).then((resp) => { - refreshFocusData.scheduledEvents = resp.events[selectedJob.job_id]; - finish(); - }).catch((resp) => { - console.log('Time series explorer - error getting scheduled events from elasticsearch:', resp); - reject(); - }); - - // Query 4 - load any annotations for the selected job. - if (mlAnnotationsEnabled) { - ml.annotations.getAnnotations({ - jobIds: [selectedJob.job_id], - earliestMs: searchBounds.min.valueOf(), - latestMs: searchBounds.max.valueOf(), - maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE - }).then((resp) => { - refreshFocusData.focusAnnotationData = resp.annotations[selectedJob.job_id] - .sort((a, b) => { - return a.timestamp - b.timestamp; - }) - .map((d, i) => { - d.key = String.fromCharCode(65 + i); - return d; - }); - - finish(); - }).catch(() => { - // silent fail - refreshFocusData.focusAnnotationData = []; - finish(); - }); - } else { - finish(); - } - - // Plus query for forecast data if there is a forecastId stored in the appState. - if (forecastId !== undefined) { - awaitingCount++; - let aggType = undefined; - const detector = selectedJob.analysis_config.detectors[detectorIndex]; - const esAgg = mlFunctionToESAggregation(detector.function); - if (modelPlotEnabled === false && (esAgg === 'sum' || esAgg === 'count')) { - aggType = { avg: 'sum', max: 'sum', min: 'sum' }; - } - - mlForecastService.getForecastData( - selectedJob, - detectorIndex, - forecastId, - nonBlankEntities, - searchBounds.min.valueOf(), - searchBounds.max.valueOf(), - focusAggregationInterval.expression, - aggType) - .then((resp) => { - refreshFocusData.focusForecastData = processForecastResults(resp.results); - refreshFocusData.showForecastCheckbox = (refreshFocusData.focusForecastData.length > 0); - finish(); - }).catch((resp) => { - console.log(`Time series explorer - error loading data for forecast ID ${forecastId}`, resp); - reject(); - }); - } - }); -}; - export function calculateAggregationInterval( bounds, bucketsTarget, From 8282450f8fa2c4f3164e39ecd5b18fa422e2ad6d Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Mon, 25 Nov 2019 13:29:49 +0100 Subject: [PATCH 044/128] [SIEM] Fix typo in Palo Alto Networks (#51570) --- .../components/page/overview/overview_network_stats/index.tsx | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx index cee2c18710e74..8f592c7bbba60 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx @@ -73,7 +73,7 @@ const overviewNetworkStats = (data: OverviewNetworkData) => [ title: ( ), id: 'filebeatPanw', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3dd141a164e3e..f35a650cdf1b6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10595,7 +10595,7 @@ "xpack.siem.overview.feedbackTitle": "フィードバック", "xpack.siem.overview.filebeatCiscoTitle": "Filebeat Cisco", "xpack.siem.overview.filebeatNetflowTitle": "Filebeat Netflow", - "xpack.siem.overview.filebeatPanwTitle": "Filebeat Palo Alto Network", + "xpack.siem.overview.filebeatPanwTitle": "Filebeat Palo Alto Networks", "xpack.siem.overview.fileBeatSuricataTitle": "Filebeat Suricata", "xpack.siem.overview.filebeatSystemModuleTitle": "Filebeat システムモジュール", "xpack.siem.overview.fileBeatZeekTitle": "Filebeat Zeek", From aef6dd4707b19babd28bb93164bdd61d8029a89a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 25 Nov 2019 09:11:08 -0500 Subject: [PATCH 045/128] Upgrade handlebars to 4.5.3 (#51486) Co-authored-by: Elastic Machine --- package.json | 2 +- x-pack/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a1873134e3cb8..a4f7b869aef6f 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "globby": "^8.0.1", "good-squeeze": "2.1.0", "h2o2": "^8.1.2", - "handlebars": "4.3.5", + "handlebars": "4.5.3", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", "history": "^4.9.0", diff --git a/x-pack/package.json b/x-pack/package.json index 84ce92bf8e9e6..f84db22fe5c40 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -244,7 +244,7 @@ "graphql-tag": "^2.9.2", "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", - "handlebars": "4.3.5", + "handlebars": "4.5.3", "history": "4.9.0", "history-extra": "^5.0.1", "i18n-iso-countries": "^4.3.1", diff --git a/yarn.lock b/yarn.lock index 64d33426d7aa4..3296fc013c48d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14310,10 +14310,10 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.3.5.tgz#d6c2d0a0f08b4479e3949f8321c0f3893bb691be" - integrity sha512-I16T/l8X9DV3sEkY9sK9lsPRgDsj82ayBY/4pAZyP2BcX5WeRM3O06bw9kIs2GLrHvFB/DNzWWJyFvof8wQGqw== +handlebars@4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" + integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== dependencies: neo-async "^2.6.0" optimist "^0.6.1" From a1b01f4a7582dd35f236393672130871a0e03f56 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 25 Nov 2019 15:14:55 +0100 Subject: [PATCH 046/128] - Update _template completion to work even when the index does not exist (#51592) - Update _template body completions --- .../components/template_autocomplete_component.js | 2 +- .../spec/overrides/indices.put_template.json | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js index e6cae3f1710bc..0c00b2f93ee6f 100644 --- a/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js +++ b/src/legacy/core_plugins/console/public/quarantined/src/autocomplete/components/template_autocomplete_component.js @@ -21,7 +21,7 @@ import { ListComponent } from './list_component'; export class TemplateAutocompleteComponent extends ListComponent { constructor(name, parent) { - super(name, mappings.getTemplates, parent); + super(name, mappings.getTemplates, parent, true, true); } getContextKey() { return 'template'; diff --git a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json index cc7218be2e48d..c19836e2f9eb0 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/overrides/indices.put_template.json @@ -1,10 +1,16 @@ { "indices.put_template": { "data_autocomplete_rules": { - "template": "index*", - "warmers": { "__scope_link": "_warmer" }, + "index_patterns": [], "mappings": { "__scope_link": "put_mapping" }, - "settings": { "__scope_link": "put_settings" } + "settings": { "__scope_link": "put_settings" }, + "version": 0, + "order": 0, + "aliases": { + "__template": { + "NAME": {} + } + } }, "patterns": [ "_template/{template}" From ce2dd0dc669af17ae84016ef374884d5be28ef1f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 25 Nov 2019 15:18:34 +0100 Subject: [PATCH 047/128] Add cumulative cardinality per https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-cardinality-aggregation.html (#51591) --- .../console/server/api_server/es_6_0/aggregations.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js index e54bc2476698a..c2596dc4258da 100644 --- a/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js +++ b/src/legacy/core_plugins/console/server/api_server/es_6_0/aggregations.js @@ -352,6 +352,13 @@ const rules = { }, missing: '', }, + cumulative_cardinality: { + __template: { + buckets_path: '', + }, + buckets_path: '', + format: '', + }, scripted_metric: { __template: { init_script: '', From 6f01aa962e3f7fe6c57d3d0ac7337c595e4e3fc9 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Mon, 25 Nov 2019 08:49:36 -0600 Subject: [PATCH 048/128] EUI i18n token updates (#51307) * updated i18n tokens for eui * i18n snapshot * translation file removals * fix typo --- .../__snapshots__/i18n_service.test.tsx.snap | 56 +++- src/core/public/i18n/i18n_service.tsx | 271 +++++++++++++++++- .../translations/translations/ja-JP.json | 4 +- .../translations/translations/zh-CN.json | 4 +- 4 files changed, 314 insertions(+), 21 deletions(-) diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index d159c588718fe..d0374511515d1 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -10,6 +10,7 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiBasicTable.selectThisRow": "Select this row", "euiBasicTable.tableDescription": [Function], "euiBottomBar.screenReaderAnnouncement": "There is a new menu opening with page level controls at the end of the document.", + "euiBreadcrumbs.collapsedBadge.ariaLabel": "Show all breadcrumbs", "euiCardSelect.select": "Select", "euiCardSelect.selected": "Selected", "euiCardSelect.unavailable": "Unavailable", @@ -21,6 +22,20 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiCollapsedItemActions.allActions": "All actions", "euiColorPicker.screenReaderAnnouncement": "A popup with a range of selectable colors opened. Tab forward to cycle through colors choices or press escape to close this popup.", "euiColorPicker.swatchAriaLabel": [Function], + "euiColorStopThumb.removeLabel": "Remove this stop", + "euiColorStopThumb.screenReaderAnnouncement": "A popup with a color stop edit form opened. Tab forward to cycle through form controls or press escape to close this popup.", + "euiColorStops.screenReaderAnnouncement": [Function], + "euiColumnSelector.hideAll": "Hide all", + "euiColumnSelector.selectAll": "Show all", + "euiColumnSorting.clearAll": "Clear sorting", + "euiColumnSorting.emptySorting": "Currently no fields are sorted", + "euiColumnSorting.pickFields": "Pick fields to sort by", + "euiColumnSorting.sortFieldAriaLabel": "Sort by:", + "euiColumnSortingDraggable.activeSortLabel": "is sorting this data grid", + "euiColumnSortingDraggable.defaultSortAsc": "A-Z", + "euiColumnSortingDraggable.defaultSortDesc": "Z-A", + "euiColumnSortingDraggable.removeSortLabel": "Remove from data grid sort:", + "euiColumnSortingDraggable.toggleLegend": "Select sorting method for field:", "euiComboBoxOptionsList.allOptionsSelected": "You've selected all available options", "euiComboBoxOptionsList.alreadyAdded": [Function], "euiComboBoxOptionsList.createCustomOption": [Function], @@ -28,6 +43,19 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiComboBoxOptionsList.noAvailableOptions": "There aren't any options available", "euiComboBoxOptionsList.noMatchingOptions": [Function], "euiComboBoxPill.removeSelection": [Function], + "euiCommonlyUsedTimeRanges.legend": "Commonly used", + "euiDataGrid.screenReaderNotice": "Cell contains interactive content.", + "euiDataGridCell.expandButtonTitle": "Click or hit enter to interact with cell content", + "euiDataGridSchema.booleanSortTextAsc": "True-False", + "euiDataGridSchema.booleanSortTextDesc": "False-True", + "euiDataGridSchema.currencySortTextAsc": "Low-High", + "euiDataGridSchema.currencySortTextDesc": "High-Low", + "euiDataGridSchema.dateSortTextAsc": "New-Old", + "euiDataGridSchema.dateSortTextDesc": "Old-New", + "euiDataGridSchema.jsonSortTextAsc": "Small-Large", + "euiDataGridSchema.jsonSortTextDesc": "Large-Small", + "euiDataGridSchema.numberSortTextAsc": "Low-High", + "euiDataGridSchema.numberSortTextDesc": "High-Low", "euiFilterButton.filterBadge": [Function], "euiForm.addressFormErrors": "Please address the errors in your form.", "euiFormControlLayoutClearButton.label": "Clear input", @@ -35,25 +63,45 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiHeaderLinks.appNavigation": "App navigation", "euiHeaderLinks.openNavigationMenu": "Open navigation menu", "euiHue.label": "Select the HSV color mode \\"hue\\" value", + "euiImage.closeImage": [Function], + "euiImage.openImage": [Function], + "euiLink.external.ariaLabel": "External link", "euiModal.closeModal": "Closes this modal window", "euiPagination.jumpToLastPage": [Function], "euiPagination.nextPage": "Next page", "euiPagination.pageOfTotal": [Function], "euiPagination.previousPage": "Previous page", - "euiPopover.screenReaderAnnouncement": "You are in a popup. To exit this popup, hit Escape.", + "euiPopover.screenReaderAnnouncement": "You are in a dialog. To close this dialog, hit escape.", + "euiQuickSelect.applyButton": "Apply", + "euiQuickSelect.fullDescription": [Function], + "euiQuickSelect.legendText": "Quick select a time range", + "euiQuickSelect.nextLabel": "Next time window", + "euiQuickSelect.previousLabel": "Previous time window", + "euiQuickSelect.quickSelectTitle": "Quick select", + "euiQuickSelect.tenseLabel": "Time tense", + "euiQuickSelect.unitLabel": "Time unit", + "euiQuickSelect.valueLabel": "Time value", + "euiRefreshInterval.fullDescription": [Function], + "euiRefreshInterval.legend": "Refresh every", + "euiRefreshInterval.start": "Start", + "euiRefreshInterval.stop": "Stop", + "euiRelativeTab.fullDescription": [Function], + "euiRelativeTab.relativeDate": [Function], + "euiRelativeTab.roundingLabel": [Function], + "euiRelativeTab.unitInputLabel": "Relative time span", "euiSaturation.roleDescription": "HSV color mode saturation and value selection", "euiSaturation.screenReaderAnnouncement": "Use the arrow keys to navigate the square color gradient. The coordinates resulting from each key press will be used to calculate HSV color mode \\"saturation\\" and \\"value\\" numbers, in the range of 0 to 1. Left and right decrease and increase (respectively) the \\"saturation\\" value. Up and down decrease and increase (respectively) the \\"value\\" value.", "euiSelectable.loadingOptions": "Loading options", "euiSelectable.noAvailableOptions": "There aren't any options available", "euiSelectable.noMatchingOptions": [Function], "euiStat.loadingText": "Statistic is loading", - "euiStep.completeStep": "Step", - "euiStep.incompleteStep": "Incomplete Step", + "euiStep.ariaLabel": [Function], "euiStepHorizontal.buttonTitle": [Function], "euiStepHorizontal.step": "Step", "euiStepNumber.hasErrors": "has errors", "euiStepNumber.hasWarnings": "has warnings", "euiStepNumber.isComplete": "complete", + "euiStyleSelector.buttonText": "Density", "euiSuperDatePicker.showDatesButtonLabel": "Show dates", "euiSuperSelect.screenReaderAnnouncement": [Function], "euiSuperSelectControl.selectAnOption": [Function], @@ -68,6 +116,8 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiToast.dismissToast": "Dismiss toast", "euiToast.newNotification": "A new notification appears", "euiToast.notification": "Notification", + "euiTreeView.ariaLabel": [Function], + "euiTreeView.listNavigationInstructions": "You can quickly navigate this list using arrow keys.", }, } } diff --git a/src/core/public/i18n/i18n_service.tsx b/src/core/public/i18n/i18n_service.tsx index 17cdf56cd43c0..721c5d49634f4 100644 --- a/src/core/public/i18n/i18n_service.tsx +++ b/src/core/public/i18n/i18n_service.tsx @@ -65,6 +65,13 @@ export class I18nService { 'Screen reader announcement that functionality is available in the page document', } ), + 'euiBreadcrumbs.collapsedBadge.ariaLabel': i18n.translate( + 'core.euiBreadcrumbs.collapsedBadge.ariaLabel', + { + defaultMessage: 'Show all breadcrumbs', + description: 'Displayed when one or more breadcrumbs are hidden.', + } + ), 'euiCardSelect.select': i18n.translate('core.euiCardSelect.select', { defaultMessage: 'Select', description: 'Displayed button text when a card option can be selected.', @@ -117,6 +124,80 @@ export class I18nService { description: 'Screen reader text to describe the action and hex value of the selectable option', }), + 'euiColorStopThumb.removeLabel': i18n.translate('core.euiColorStopThumb.removeLabel', { + defaultMessage: 'Remove this stop', + description: 'Label accompanying a button whose action will remove the color stop', + }), + 'euiColorStopThumb.screenReaderAnnouncement': i18n.translate( + 'core.euiColorStopThumb.screenReaderAnnouncement', + { + defaultMessage: + 'A popup with a color stop edit form opened. Tab forward to cycle through form controls or press escape to close this popup.', + description: + 'Message when the color picker popover has opened for an individual color stop thumb.', + } + ), + 'euiColorStops.screenReaderAnnouncement': ({ label, readOnly, disabled }: EuiValues) => + i18n.translate('core.euiColorStops.screenReaderAnnouncement', { + defaultMessage: + '{label}: {readOnly} {disabled} Color stop picker. Each stop consists of a number and corresponding color value. Use the Down and Up arrow keys to select individual stops. Press the Enter key to create a new stop.', + values: { label, readOnly, disabled }, + description: + 'Screen reader text to describe the composite behavior of the color stops component.', + }), + 'euiColumnSelector.hideAll': i18n.translate('core.euiColumnSelector.hideAll', { + defaultMessage: 'Hide all', + }), + 'euiColumnSelector.selectAll': i18n.translate('core.euiColumnSelector.selectAll', { + defaultMessage: 'Show all', + }), + 'euiColumnSorting.clearAll': i18n.translate('core.euiColumnSorting.clearAll', { + defaultMessage: 'Clear sorting', + }), + 'euiColumnSorting.emptySorting': i18n.translate('core.euiColumnSorting.emptySorting', { + defaultMessage: 'Currently no fields are sorted', + }), + 'euiColumnSorting.pickFields': i18n.translate('core.euiColumnSorting.pickFields', { + defaultMessage: 'Pick fields to sort by', + }), + 'euiColumnSorting.sortFieldAriaLabel': i18n.translate( + 'core.euiColumnSorting.sortFieldAriaLabel', + { + defaultMessage: 'Sort by:', + } + ), + 'euiColumnSortingDraggable.activeSortLabel': i18n.translate( + 'core.euiColumnSortingDraggable.activeSortLabel', + { + defaultMessage: 'is sorting this data grid', + } + ), + 'euiColumnSortingDraggable.defaultSortAsc': i18n.translate( + 'core.euiColumnSortingDraggable.defaultSortAsc', + { + defaultMessage: 'A-Z', + description: 'Ascending sort label', + } + ), + 'euiColumnSortingDraggable.defaultSortDesc': i18n.translate( + 'core.euiColumnSortingDraggable.defaultSortDesc', + { + defaultMessage: 'Z-A', + description: 'Descending sort label', + } + ), + 'euiColumnSortingDraggable.removeSortLabel': i18n.translate( + 'core.euiColumnSortingDraggable.removeSortLabel', + { + defaultMessage: 'Remove from data grid sort:', + } + ), + 'euiColumnSortingDraggable.toggleLegend': i18n.translate( + 'core.euiColumnSortingDraggable.toggleLegend', + { + defaultMessage: 'Select sorting method for field:', + } + ), 'euiComboBoxOptionsList.allOptionsSelected': i18n.translate( 'core.euiComboBoxOptionsList.allOptionsSelected', { @@ -163,6 +244,88 @@ export class I18nService { values: { children }, description: 'ARIA label, `children` is the human-friendly value of an option', }), + 'euiCommonlyUsedTimeRanges.legend': i18n.translate('core.euiCommonlyUsedTimeRanges.legend', { + defaultMessage: 'Commonly used', + }), + 'euiDataGrid.screenReaderNotice': i18n.translate('core.euiDataGrid.screenReaderNotice', { + defaultMessage: 'Cell contains interactive content.', + }), + 'euiDataGridCell.expandButtonTitle': i18n.translate( + 'core.euiDataGridCell.expandButtonTitle', + { + defaultMessage: 'Click or hit enter to interact with cell content', + } + ), + 'euiDataGridSchema.booleanSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.booleanSortTextAsc', + { + defaultMessage: 'True-False', + description: 'Ascending boolean label', + } + ), + 'euiDataGridSchema.booleanSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.booleanSortTextDesc', + { + defaultMessage: 'False-True', + description: 'Descending boolean label', + } + ), + 'euiDataGridSchema.currencySortTextAsc': i18n.translate( + 'core.euiDataGridSchema.currencySortTextAsc', + { + defaultMessage: 'Low-High', + description: 'Ascending currency label', + } + ), + 'euiDataGridSchema.currencySortTextDesc': i18n.translate( + 'core.euiDataGridSchema.currencySortTextDesc', + { + defaultMessage: 'High-Low', + description: 'Descending currency label', + } + ), + 'euiDataGridSchema.dateSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.dateSortTextAsc', + { + defaultMessage: 'New-Old', + description: 'Ascending date label', + } + ), + 'euiDataGridSchema.dateSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.dateSortTextDesc', + { + defaultMessage: 'Old-New', + description: 'Descending date label', + } + ), + 'euiDataGridSchema.numberSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.numberSortTextAsc', + { + defaultMessage: 'Low-High', + description: 'Ascending number label', + } + ), + 'euiDataGridSchema.numberSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.numberSortTextDesc', + { + defaultMessage: 'High-Low', + description: 'Descending number label', + } + ), + 'euiDataGridSchema.jsonSortTextAsc': i18n.translate( + 'core.euiDataGridSchema.jsonSortTextAsc', + { + defaultMessage: 'Small-Large', + description: 'Ascending size label', + } + ), + 'euiDataGridSchema.jsonSortTextDesc': i18n.translate( + 'core.euiDataGridSchema.jsonSortTextDesc', + { + defaultMessage: 'Large-Small', + description: 'Descending size label', + } + ), 'euiFilterButton.filterBadge': ({ count, hasActiveFilters }: EuiValues) => i18n.translate('core.euiFilterButton.filterBadge', { defaultMessage: '${count} ${filterCountLabel} filters', @@ -195,6 +358,19 @@ export class I18nService { 'euiHue.label': i18n.translate('core.euiHue.label', { defaultMessage: 'Select the HSV color mode "hue" value', }), + 'euiImage.closeImage': ({ alt }: EuiValues) => + i18n.translate('core.euiImage.closeImage', { + defaultMessage: 'Close full screen {alt} image', + values: { alt }, + }), + 'euiImage.openImage': ({ alt }: EuiValues) => + i18n.translate('core.euiImage.openImage', { + defaultMessage: 'Open full screen {alt} image', + values: { alt }, + }), + 'euiLink.external.ariaLabel': i18n.translate('core.euiLink.external.ariaLabel', { + defaultMessage: 'External link', + }), 'euiModal.closeModal': i18n.translate('core.euiModal.closeModal', { defaultMessage: 'Closes this modal window', }), @@ -217,9 +393,70 @@ export class I18nService { 'euiPopover.screenReaderAnnouncement': i18n.translate( 'core.euiPopover.screenReaderAnnouncement', { - defaultMessage: 'You are in a popup. To exit this popup, hit Escape.', + defaultMessage: 'You are in a dialog. To close this dialog, hit escape.', } ), + 'euiQuickSelect.applyButton': i18n.translate('core.euiQuickSelect.applyButton', { + defaultMessage: 'Apply', + }), + 'euiQuickSelect.fullDescription': ({ timeTense, timeValue, timeUnit }: EuiValues) => + i18n.translate('core.euiQuickSelect.fullDescription', { + defaultMessage: 'Currently set to {timeTense} {timeValue} {timeUnit}.', + values: { timeTense, timeValue, timeUnit }, + }), + 'euiQuickSelect.legendText': i18n.translate('core.euiQuickSelect.legendText', { + defaultMessage: 'Quick select a time range', + }), + 'euiQuickSelect.nextLabel': i18n.translate('core.euiQuickSelect.nextLabel', { + defaultMessage: 'Next time window', + }), + 'euiQuickSelect.previousLabel': i18n.translate('core.euiQuickSelect.previousLabel', { + defaultMessage: 'Previous time window', + }), + 'euiQuickSelect.quickSelectTitle': i18n.translate('core.euiQuickSelect.quickSelectTitle', { + defaultMessage: 'Quick select', + }), + 'euiQuickSelect.tenseLabel': i18n.translate('core.euiQuickSelect.tenseLabel', { + defaultMessage: 'Time tense', + }), + 'euiQuickSelect.unitLabel': i18n.translate('core.euiQuickSelect.unitLabel', { + defaultMessage: 'Time unit', + }), + 'euiQuickSelect.valueLabel': i18n.translate('core.euiQuickSelect.valueLabel', { + defaultMessage: 'Time value', + }), + 'euiRefreshInterval.fullDescription': ({ optionValue, optionText }: EuiValues) => + i18n.translate('core.euiRefreshInterval.fullDescription', { + defaultMessage: 'Currently set to {optionValue} {optionText}.', + values: { optionValue, optionText }, + }), + 'euiRefreshInterval.legend': i18n.translate('core.euiRefreshInterval.legend', { + defaultMessage: 'Refresh every', + }), + 'euiRefreshInterval.start': i18n.translate('core.euiRefreshInterval.start', { + defaultMessage: 'Start', + }), + 'euiRefreshInterval.stop': i18n.translate('core.euiRefreshInterval.stop', { + defaultMessage: 'Stop', + }), + 'euiRelativeTab.fullDescription': ({ unit }: EuiValues) => + i18n.translate('core.euiRelativeTab.fullDescription', { + defaultMessage: 'The unit is changeable. Currently set to {unit}.', + values: { unit }, + }), + 'euiRelativeTab.relativeDate': ({ position }: EuiValues) => + i18n.translate('core.euiRelativeTab.relativeDate', { + defaultMessage: '{position} date', + values: { position }, + }), + 'euiRelativeTab.roundingLabel': ({ unit }: EuiValues) => + i18n.translate('core.euiRelativeTab.roundingLabel', { + defaultMessage: 'Round to the {unit}', + values: { unit }, + }), + 'euiRelativeTab.unitInputLabel': i18n.translate('core.euiRelativeTab.unitInputLabel', { + defaultMessage: 'Relative time span', + }), 'euiSaturation.roleDescription': i18n.translate('core.euiSaturation.roleDescription', { defaultMessage: 'HSV color mode saturation and value selection', }), @@ -247,22 +484,18 @@ export class I18nService { 'euiStat.loadingText': i18n.translate('core.euiStat.loadingText', { defaultMessage: 'Statistic is loading', }), - 'euiStep.completeStep': i18n.translate('core.euiStep.completeStep', { - defaultMessage: 'Step', - description: - 'See https://elastic.github.io/eui/#/navigation/steps to know how Step control looks like', - }), - 'euiStep.incompleteStep': i18n.translate('core.euiStep.incompleteStep', { - defaultMessage: 'Incomplete Step', - }), + 'euiStep.ariaLabel': ({ status }: EuiValues) => + i18n.translate('core.euiStep.ariaLabel', { + defaultMessage: '{stepStatus}', + values: { stepStatus: status === 'incomplete' ? 'Incomplete Step' : 'Step' }, + }), 'euiStepHorizontal.buttonTitle': ({ step, title, disabled, isComplete }: EuiValues) => { return i18n.translate('core.euiStepHorizontal.buttonTitle', { - defaultMessage: - 'Step {step}: {title}{titleAppendix, select, completed { is completed} disabled { is disabled} other {}}', + defaultMessage: 'Step {step}: {title}{titleAppendix}', values: { step, title, - titleAppendix: disabled ? 'disabled' : isComplete ? 'completed' : '', + titleAppendix: disabled ? ' is disabled' : isComplete ? ' is complete' : '', }, }); }, @@ -285,6 +518,9 @@ export class I18nService { description: 'Used as the title attribute on an image or svg icon to indicate a given process step is complete', }), + 'euiStyleSelector.buttonText': i18n.translate('core.euiStyleSelector.buttonText', { + defaultMessage: 'Density', + }), 'euiSuperDatePicker.showDatesButtonLabel': i18n.translate( 'core.euiSuperDatePicker.showDatesButtonLabel', { @@ -362,6 +598,17 @@ export class I18nService { defaultMessage: 'Notification', description: 'ARIA label on an element containing a notification', }), + 'euiTreeView.ariaLabel': ({ nodeLabel, ariaLabel }: EuiValues) => + i18n.translate('core.euiTreeView.ariaLabel', { + defaultMessage: '{nodeLabel} child of {ariaLabel}', + values: { nodeLabel, ariaLabel }, + }), + 'euiTreeView.listNavigationInstructions': i18n.translate( + 'core.euiTreeView.listNavigationInstructions', + { + defaultMessage: 'You can quickly navigate this list using arrow keys.', + } + ), }; return { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f35a650cdf1b6..f83d0c9ea3c9a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -616,8 +616,6 @@ "core.euiSelectable.noAvailableOptions": "利用可能なオプションがありません", "core.euiSelectable.noMatchingOptions": "{searchValue} はどのオプションにも一致していません", "core.euiStat.loadingText": "統計を読み込み中です", - "core.euiStep.completeStep": "手順", - "core.euiStep.incompleteStep": "未完了の手順", "core.euiStepHorizontal.buttonTitle": "ステップ {step}: {title}{titleAppendix, select, completed { が完了} 無効 { が無効} other {}}", "core.euiStepHorizontal.step": "手順", "core.euiStepNumber.hasErrors": "エラーがあります", @@ -12759,4 +12757,4 @@ "xpack.licensing.check.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", "xpack.licensing.check.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fb5b7cc191e61..a830eaacd29e3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -617,8 +617,6 @@ "core.euiSelectable.noAvailableOptions": "没有任何可用选项", "core.euiSelectable.noMatchingOptions": "{searchValue} 不匹配任何选项", "core.euiStat.loadingText": "统计正在加载", - "core.euiStep.completeStep": "步骤", - "core.euiStep.incompleteStep": "未完成步骤", "core.euiStepHorizontal.buttonTitle": "第 {step} 步:{title}{titleAppendix, select, completed {已完成} disabled {已禁用} other {}}", "core.euiStepHorizontal.step": "步骤", "core.euiStepNumber.hasErrors": "有错误", @@ -12848,4 +12846,4 @@ "xpack.licensing.check.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", "xpack.licensing.check.errorUnsupportedMessage": "您的{licenseType}许可证不支持 {pluginName}。请升级您的许可证。" } -} \ No newline at end of file +} From 132f6b1e0900e22293d99409769cef4ff800a268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Mon, 25 Nov 2019 10:24:09 -0500 Subject: [PATCH 049/128] Rename alertTypeParams to params (#51255) --- x-pack/legacy/plugins/alerting/README.md | 4 +- x-pack/legacy/plugins/alerting/mappings.json | 2 +- .../alerting/server/alerts_client.test.ts | 298 +++++++++--------- .../plugins/alerting/server/alerts_client.ts | 10 +- .../server/lib/task_runner_factory.test.ts | 4 +- .../server/lib/task_runner_factory.ts | 4 +- .../lib/validate_alert_type_params.test.ts | 2 +- .../server/lib/validate_alert_type_params.ts | 2 +- .../alerting/server/routes/create.test.ts | 20 +- .../plugins/alerting/server/routes/create.ts | 4 +- .../alerting/server/routes/get.test.ts | 2 +- .../alerting/server/routes/update.test.ts | 10 +- .../plugins/alerting/server/routes/update.ts | 4 +- .../legacy/plugins/alerting/server/types.ts | 4 +- .../detection_engine/alerts/create_signals.ts | 2 +- .../alerts/read_signals.test.ts | 32 +- .../detection_engine/alerts/read_signals.ts | 2 +- .../lib/detection_engine/alerts/types.ts | 2 +- .../detection_engine/alerts/update_signals.ts | 8 +- .../routes/__mocks__/request_responses.ts | 2 +- .../lib/detection_engine/routes/utils.test.ts | 10 +- .../lib/detection_engine/routes/utils.ts | 42 +-- .../common/lib/alert_utils.ts | 2 +- .../common/lib/get_test_alert_data.ts | 2 +- .../tests/alerting/alerts.ts | 10 +- .../tests/alerting/create.ts | 10 +- .../tests/alerting/find.ts | 4 +- .../security_and_spaces/tests/alerting/get.ts | 2 +- .../tests/alerting/update.ts | 16 +- .../spaces_only/tests/alerting/alerts.ts | 6 +- .../spaces_only/tests/alerting/create.ts | 2 +- .../spaces_only/tests/alerting/find.ts | 2 +- .../spaces_only/tests/alerting/get.ts | 2 +- .../spaces_only/tests/alerting/update.ts | 4 +- .../es_archives/hybrid/kibana/mappings.json | 4 +- .../es_archives/lens/basic/mappings.json | 4 +- .../es_archives/lens/reporting/mappings.json | 4 +- .../es_archives/ml/farequote/mappings.json | 2 +- .../es_archives/reporting/nanos/mappings.json | 4 +- 39 files changed, 275 insertions(+), 275 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 456eb6732c81c..40f61d11e9ace 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -202,7 +202,7 @@ Payload: |tags|A list of keywords to reference and search in the future.|string[]| |alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string| |interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| -|alertTypeParams|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| +|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
    - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
    - `id` (string): The id of the action saved object to execute.
    - `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| #### `DELETE /api/alert/{id}`: Delete alert @@ -246,7 +246,7 @@ Payload: |interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| |name|A name to reference and search in the future.|string| |tags|A list of keywords to reference and search in the future.|string[]| -|alertTypeParams|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| +|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
    - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
    - `id` (string): The id of the action saved object to execute.
    - `params` (object): There map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| #### `POST /api/alert/{id}/_enable`: Enable an alert diff --git a/x-pack/legacy/plugins/alerting/mappings.json b/x-pack/legacy/plugins/alerting/mappings.json index 7a1be777aff44..f840c019d5e02 100644 --- a/x-pack/legacy/plugins/alerting/mappings.json +++ b/x-pack/legacy/plugins/alerting/mappings.json @@ -31,7 +31,7 @@ } } }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index dc3aaaf5cf23c..08607f04a5235 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -48,7 +48,7 @@ function getMockData(overwrites: Record = {}) { alertTypeId: '123', interval: '10s', throttle: null, - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -80,7 +80,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -130,25 +130,25 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); @@ -164,9 +164,6 @@ describe('create()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": undefined, "apiKeyOwner": undefined, "createdBy": "elastic", @@ -175,6 +172,9 @@ describe('create()', () => { "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -240,7 +240,7 @@ describe('create()', () => { enabled: false, alertTypeId: '123', interval: 10000, - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -263,30 +263,30 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "enabled": false, - "id": "1", - "interval": 10000, - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "enabled": false, + "id": "1", + "interval": 10000, + "params": Object { + "bar": true, + }, + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); - test('should validate alertTypeParams', async () => { + test('should validate params', async () => { const alertsClient = new AlertsClient(alertsClientParams); const data = getMockData(); alertTypeRegistry.get.mockReturnValueOnce({ @@ -302,7 +302,7 @@ describe('create()', () => { async executor() {}, }); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); @@ -337,7 +337,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -387,7 +387,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -448,7 +448,7 @@ describe('create()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -511,7 +511,7 @@ describe('create()', () => { ], alertTypeId: '123', name: 'abc', - alertTypeParams: { bar: true }, + params: { bar: true }, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', createdBy: 'elastic', @@ -923,7 +923,7 @@ describe('get()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -946,24 +946,24 @@ describe('get()', () => { }); const result = await alertsClient.get({ id: '1' }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + } + `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -981,7 +981,7 @@ describe('get()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1016,7 +1016,7 @@ describe('find()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1041,31 +1041,31 @@ describe('find()', () => { }); const result = await alertsClient.find(); expect(result).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); + Object { + "data": Array [ + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -1086,7 +1086,7 @@ describe('delete()', () => { attributes: { alertTypeId: '123', interval: '10s', - alertTypeParams: { + params: { bar: true, }, scheduledTaskId: 'task-123', @@ -1155,7 +1155,7 @@ describe('update()', () => { attributes: { enabled: true, interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1183,7 +1183,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1198,25 +1198,25 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); @@ -1233,14 +1233,14 @@ describe('update()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": null, "apiKeyOwner": null, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1291,7 +1291,7 @@ describe('update()', () => { attributes: { enabled: true, interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1320,7 +1320,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1335,26 +1335,26 @@ describe('update()', () => { }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "apiKey": "MTIzOmFiYw==", - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": "MTIzOmFiYw==", + "enabled": true, + "id": "1", + "interval": "10s", + "params": Object { + "bar": true, + }, + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); @@ -1371,14 +1371,14 @@ describe('update()', () => { }, ], "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, "apiKey": "MTIzOmFiYw==", "apiKeyOwner": "elastic", "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1400,7 +1400,7 @@ describe('update()', () => { `); }); - it('should validate alertTypeParams', async () => { + it('should validate params', async () => { const alertsClient = new AlertsClient(alertsClientParams); alertTypeRegistry.get.mockReturnValueOnce({ id: '123', @@ -1428,7 +1428,7 @@ describe('update()', () => { interval: '10s', name: 'abc', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -1443,7 +1443,7 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); }); diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index c260a754e4594..3916ec1d62b6c 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -77,7 +77,7 @@ interface UpdateOptions { tags: string[]; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; }; } @@ -111,7 +111,7 @@ export class AlertsClient { public async create({ data, options }: CreateOptions) { // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); const apiKey = await this.createAPIKey(); const username = await this.getUserName(); @@ -125,7 +125,7 @@ export class AlertsClient { apiKey: apiKey.created ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') : undefined, - alertTypeParams: validatedAlertTypeParams, + params: validatedAlertTypeParams, muteAll: false, mutedInstanceIds: [], }); @@ -199,7 +199,7 @@ export class AlertsClient { const apiKey = await this.createAPIKey(); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); this.validateActions(alertType, data.actions); const { actions, references } = this.extractReferences(data.actions); @@ -210,7 +210,7 @@ export class AlertsClient { { ...attributes, ...data, - alertTypeParams: validatedAlertTypeParams, + params: validatedAlertTypeParams, actions, updatedBy: username, apiKeyOwner: apiKey.created ? username : null, diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts index dcc74ed9488ce..1d91d4a35d588 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts @@ -76,7 +76,7 @@ const mockedAlertTypeSavedObject = { alertTypeId: '123', interval: '10s', mutedInstanceIds: [], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -253,7 +253,7 @@ test('validates params before executing the alert type', async () => { references: [], }); await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts index 66d445f57fe73..051b15fc8dd8f 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts @@ -94,12 +94,12 @@ export class TaskRunnerFactory { const services = getServices(fakeRequest); // Ensure API key is still valid and user has access const { - attributes: { alertTypeParams, actions, interval, throttle, muteAll, mutedInstanceIds }, + attributes: { params, actions, interval, throttle, muteAll, mutedInstanceIds }, references, } = await services.savedObjectsClient.get('alert', alertId); // Validate - const validatedAlertTypeParams = validateAlertTypeParams(alertType, alertTypeParams); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, params); // Inject ids into actions const actionsWithIds = actions.map(action => { diff --git a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts index f33746798769b..e9a61354001f1 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.test.ts @@ -61,6 +61,6 @@ test('should validate and throw error when params is invalid', () => { {} ) ).toThrowErrorMatchingInlineSnapshot( - `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts index 6070f2d99b605..248d896c06ac2 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/validate_alert_type_params.ts @@ -19,6 +19,6 @@ export function validateAlertTypeParams>( try { return validator.validate(params); } catch (err) { - throw Boom.badRequest(`alertTypeParams invalid: ${err.message}`); + throw Boom.badRequest(`params invalid: ${err.message}`); } } diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts index c67d1a7b32352..318dbdf068d6a 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts @@ -15,7 +15,7 @@ const mockedAlert = { name: 'abc', interval: '10s', tags: ['foo'], - alertTypeParams: { + params: { bar: true, }, actions: [ @@ -57,12 +57,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "id": "123", "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -83,12 +83,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], @@ -112,12 +112,12 @@ test('creates an alert with proper parameters', async () => { }, ], "alertTypeId": "1", - "alertTypeParams": Object { - "bar": true, - }, "enabled": true, "interval": "10s", "name": "abc", + "params": Object { + "bar": true, + }, "tags": Array [ "foo", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.ts b/x-pack/legacy/plugins/alerting/server/routes/create.ts index 65fbae7c8b298..fb82a03f172b3 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.ts @@ -17,7 +17,7 @@ interface ScheduleRequest extends Hapi.Request { alertTypeId: string; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; throttle: string | null; }; } @@ -41,7 +41,7 @@ export const createAlertRoute = { alertTypeId: Joi.string().required(), throttle: getDurationSchema().default(null), interval: getDurationSchema().required(), - alertTypeParams: Joi.object().required(), + params: Joi.object().required(), actions: Joi.array() .items( Joi.object().keys({ diff --git a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts index 84938a0e927d1..19618bc9e39fe 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts @@ -14,7 +14,7 @@ const mockedAlert = { id: '1', alertTypeId: '1', interval: '10s', - alertTypeParams: { + params: { bar: true, }, actions: [ diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts index ee98f7d6dd9d3..7fc3f45911010 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts @@ -17,7 +17,7 @@ const mockedResponse = { alertTypeId: '1', tags: ['foo'], interval: '12s', - alertTypeParams: { + params: { otherField: false, }, actions: [ @@ -40,7 +40,7 @@ test('calls the update function with proper parameters', async () => { name: 'abc', tags: ['bar'], interval: '12s', - alertTypeParams: { + params: { otherField: false, }, actions: [ @@ -74,11 +74,11 @@ test('calls the update function with proper parameters', async () => { }, }, ], - "alertTypeParams": Object { - "otherField": false, - }, "interval": "12s", "name": "abc", + "params": Object { + "otherField": false, + }, "tags": Array [ "bar", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.ts b/x-pack/legacy/plugins/alerting/server/routes/update.ts index 9c8e0296c2f78..6aeedb93a1098 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.ts @@ -19,7 +19,7 @@ interface UpdateRequest extends Hapi.Request { tags: string[]; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; throttle: string | null; }; } @@ -43,7 +43,7 @@ export const updateAlertRoute = { .items(Joi.string()) .required(), interval: getDurationSchema().required(), - alertTypeParams: Joi.object().required(), + params: Joi.object().required(), actions: Joi.array() .items( Joi.object().keys({ diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 359b88e21cc3b..e2460c549c05d 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -65,7 +65,7 @@ export interface Alert { alertTypeId: string; interval: string; actions: AlertAction[]; - alertTypeParams: Record; + params: Record; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; @@ -83,7 +83,7 @@ export interface RawAlert extends SavedObjectAttributes { alertTypeId: string; interval: string; actions: RawAlertAction[]; - alertTypeParams: SavedObjectAttributes; + params: SavedObjectAttributes; scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts index 9f472d060def7..420f995431423 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts @@ -40,7 +40,7 @@ export const createSignals = async ({ name, tags: [], alertTypeId: SIGNALS_ID, - alertTypeParams: { + params: { description, ruleId, index, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts index dde3f19b1c66d..39d1fac8f7a09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts @@ -129,11 +129,11 @@ describe('read_signals', () => { test('should return a single value of rule-1 with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -150,11 +150,11 @@ describe('read_signals', () => { test('should return a single value of rule-2 with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -171,11 +171,11 @@ describe('read_signals', () => { test('should return null for a made up value with multiple values', async () => { const result1 = getResult(); result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.alertTypeParams.ruleId = 'rule-1'; + result1.params.ruleId = 'rule-1'; const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.alertTypeParams.ruleId = 'rule-2'; + result2.params.ruleId = 'rule-2'; const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -194,8 +194,8 @@ describe('read_signals', () => { test('returns null if the objects are not of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: 'made up 1', alertTypeParams: { ruleId: '123' } }, - { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, + { alertTypeId: 'made up 1', params: { ruleId: '123' } }, + { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); @@ -205,30 +205,30 @@ describe('read_signals', () => { test('returns correct type if the objects are of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: 'made up 2', alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '123' } }); + expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); }); test('returns second correct type if the objects are of a signal rule type', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '456' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', alertTypeParams: { ruleId: '456' } }); + expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); }); test('returns null with correct types but data does not exist', () => { const signal = findSignalInArrayByRuleId( [ - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, alertTypeParams: { ruleId: '456' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, + { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '892' ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts index f73074b560cb2..3c49112aaf50b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts @@ -14,7 +14,7 @@ export const findSignalInArrayByRuleId = ( if (isAlertTypeArray(objects)) { const signals: SignalAlertType[] = objects; const signal: SignalAlertType[] = signals.filter(datum => { - return datum.alertTypeParams.ruleId === ruleId; + return datum.params.ruleId === ruleId; }); if (signal.length !== 0) { return signal[0]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 79e62538b1a7e..9c6e1f99c672b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -137,7 +137,7 @@ export type AlertTypeParams = Omit ({ name: 'Detect Root/Admin Users', tags: [], alertTypeId: 'siem.signals', - alertTypeParams: { + params: { description: 'Detecting root and admin users', ruleId: 'rule-1', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 3d7f0a9fd049a..22dd7be5fbba7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -75,7 +75,7 @@ describe('utils', () => { test('should omit query if query is null', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = null; + fullSignal.params.query = null; const signal = transformAlertToSignal(fullSignal); expect(signal).toEqual({ created_by: 'elastic', @@ -105,7 +105,7 @@ describe('utils', () => { test('should omit query if query is undefined', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = undefined; + fullSignal.params.query = undefined; const signal = transformAlertToSignal(fullSignal); expect(signal).toEqual({ created_by: 'elastic', @@ -135,8 +135,8 @@ describe('utils', () => { test('should omit a mix of undefined, null, and missing fields', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.query = undefined; - fullSignal.alertTypeParams.language = null; + fullSignal.params.query = undefined; + fullSignal.params.language = null; const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); expect(omitData).toEqual({ created_by: 'elastic', @@ -194,7 +194,7 @@ describe('utils', () => { test('should return immutable is equal to false', () => { const fullSignal = getResult(); - fullSignal.alertTypeParams.immutable = false; + fullSignal.params.immutable = false; const signalWithEnabledFalse = transformAlertToSignal(fullSignal); expect(signalWithEnabledFalse).toEqual({ created_by: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index bf39d9d16b2b9..e3a677741efca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -29,32 +29,32 @@ export const getIdError = ({ export const transformAlertToSignal = (signal: SignalAlertType): Partial => { return pickBy((value: unknown) => value != null, { created_by: signal.createdBy, - description: signal.alertTypeParams.description, + description: signal.params.description, enabled: signal.enabled, - false_positives: signal.alertTypeParams.falsePositives, - filter: signal.alertTypeParams.filter, - filters: signal.alertTypeParams.filters, - from: signal.alertTypeParams.from, + false_positives: signal.params.falsePositives, + filter: signal.params.filter, + filters: signal.params.filters, + from: signal.params.from, id: signal.id, - immutable: signal.alertTypeParams.immutable, - index: signal.alertTypeParams.index, + immutable: signal.params.immutable, + index: signal.params.index, interval: signal.interval, - rule_id: signal.alertTypeParams.ruleId, - language: signal.alertTypeParams.language, - output_index: signal.alertTypeParams.outputIndex, - max_signals: signal.alertTypeParams.maxSignals, - risk_score: signal.alertTypeParams.riskScore, + rule_id: signal.params.ruleId, + language: signal.params.language, + output_index: signal.params.outputIndex, + max_signals: signal.params.maxSignals, + risk_score: signal.params.riskScore, name: signal.name, - query: signal.alertTypeParams.query, - references: signal.alertTypeParams.references, - saved_id: signal.alertTypeParams.savedId, - meta: signal.alertTypeParams.meta, - severity: signal.alertTypeParams.severity, - size: signal.alertTypeParams.size, + query: signal.params.query, + references: signal.params.references, + saved_id: signal.params.savedId, + meta: signal.params.meta, + severity: signal.params.severity, + size: signal.params.size, updated_by: signal.updatedBy, - tags: signal.alertTypeParams.tags, - to: signal.alertTypeParams.to, - type: signal.alertTypeParams.type, + tags: signal.params.tags, + to: signal.params.to, + type: signal.params.type, }); }; diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 4fbb13b229003..57b4b3b6c26c6 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -183,7 +183,7 @@ export class AlertUtils { throttle: '1m', tags: [], alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, diff --git a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts index d7fba7e43c372..ae382652b6234 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_test_alert_data.ts @@ -13,7 +13,7 @@ export function getTestAlertData(overwrites = {}) { interval: '1m', throttle: '1m', actions: [], - alertTypeParams: {}, + params: {}, ...overwrites, }; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index c43e159bbe8ca..09a642d1d14bb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -166,7 +166,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference: 'create-test-2', }, @@ -258,7 +258,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.authorization', - alertTypeParams: { + params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', savedObjectsClientId: '1', @@ -356,7 +356,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, @@ -491,7 +491,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, overwrites: { interval: '1s', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, groupsToScheduleActionsInSeries: ['default', 'other'], @@ -560,7 +560,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, overwrites: { interval: '1s', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, groupsToScheduleActionsInSeries: ['default', null, 'default'], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index d94556d6cedda..bf61ee2e3f137 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -59,7 +59,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { actions: [], enabled: true, alertTypeId: 'test.noop', - alertTypeParams: {}, + params: {}, createdBy: user.username, interval: '1m', scheduledTaskId: response.body.scheduledTaskId, @@ -173,10 +173,10 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "name" fails because ["name" is required]. child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]', + 'child "name" fails because ["name" is required]. child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['name', 'alertTypeId', 'interval', 'alertTypeParams', 'actions'], + keys: ['name', 'alertTypeId', 'interval', 'params', 'actions'], }, }); break; @@ -185,7 +185,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); - it(`should handle create alert request appropriately when alertTypeParams isn't valid`, async () => { + it(`should handle create alert request appropriately when params isn't valid`, async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') @@ -214,7 +214,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]', + 'params invalid: [param1]: expected value of type [string] but got [undefined]', }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index b04c0f44e7dd4..31af7a0acffbb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -62,7 +62,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: match.scheduledTaskId, throttle: '1m', @@ -119,7 +119,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: match.scheduledTaskId, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index cfb2f34ca8056..1a8109f6b6b3c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -56,7 +56,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: 'elastic', scheduledTaskId: response.body.scheduledTaskId, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 78f70ddb13edd..1b1bcef9ad23f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -33,7 +33,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const updatedData = { name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -93,7 +93,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -142,7 +142,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { tags: ['bar'], throttle: '1m', alertTypeId: '1', - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -203,10 +203,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]', + 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['throttle', 'name', 'tags', 'interval', 'alertTypeParams', 'actions'], + keys: ['throttle', 'name', 'tags', 'interval', 'params', 'actions'], }, }); break; @@ -222,7 +222,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.validation', - alertTypeParams: { + params: { param1: 'test', }, }) @@ -239,7 +239,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { tags: ['bar'], interval: '1m', throttle: '1m', - alertTypeParams: {}, + params: {}, actions: [], }); @@ -261,7 +261,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]', + 'params invalid: [param1]: expected value of type [string] but got [undefined]', }); break; default: diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts index 28634c46b6350..9af4848c57d7d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts @@ -125,7 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) { getTestAlertData({ interval: '1m', alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference: 'create-test-2', }, @@ -193,7 +193,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.authorization', - alertTypeParams: { + params: { callClusterAuthorizationIndex: authorizationIndex, savedObjectsClientType: 'dashboard', savedObjectsClientId: '1', @@ -238,7 +238,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.always-firing', - alertTypeParams: { + params: { index: ES_TEST_INDEX_NAME, reference, }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 80459690af732..3018f8efffffe 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -41,7 +41,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { actions: [], enabled: true, alertTypeId: 'test.noop', - alertTypeParams: {}, + params: {}, createdBy: null, interval: '1m', scheduledTaskId: response.body.scheduledTaskId, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index f49d774fc1e92..0d12af6db79b2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -45,7 +45,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: null, scheduledTaskId: match.scheduledTaskId, updatedBy: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index ef27a2713e98a..9e4797bcbf7ad 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -39,7 +39,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { interval: '1m', enabled: true, actions: [], - alertTypeParams: {}, + params: {}, createdBy: null, scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index 942eff0766722..a6eccf88d9e26 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -28,7 +28,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const updatedData = { name: 'bcd', tags: ['bar'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', @@ -68,7 +68,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['foo'], - alertTypeParams: { + params: { foo: true, }, interval: '12s', diff --git a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json index 18b359d37aaa6..5256e29956f4f 100644 --- a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json @@ -99,7 +99,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1068,4 +1068,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/basic/mappings.json b/x-pack/test/functional/es_archives/lens/basic/mappings.json index b87dbe12a7005..f2a29f022ff5e 100644 --- a/x-pack/test/functional/es_archives/lens/basic/mappings.json +++ b/x-pack/test/functional/es_archives/lens/basic/mappings.json @@ -100,7 +100,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1291,4 +1291,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/reporting/mappings.json b/x-pack/test/functional/es_archives/lens/reporting/mappings.json index 0321d57bc2df6..8b8e5a0e6e7f6 100644 --- a/x-pack/test/functional/es_archives/lens/reporting/mappings.json +++ b/x-pack/test/functional/es_archives/lens/reporting/mappings.json @@ -100,7 +100,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1300,4 +1300,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/ml/farequote/mappings.json b/x-pack/test/functional/es_archives/ml/farequote/mappings.json index 4fe559cc85fe1..b00545c015a74 100644 --- a/x-pack/test/functional/es_archives/ml/farequote/mappings.json +++ b/x-pack/test/functional/es_archives/ml/farequote/mappings.json @@ -133,7 +133,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, diff --git a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json index 34420b6bb63e1..dd717387a2643 100644 --- a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json @@ -84,7 +84,7 @@ "alertTypeId": { "type": "keyword" }, - "alertTypeParams": { + "params": { "enabled": false, "type": "object" }, @@ -1091,4 +1091,4 @@ } } } -} \ No newline at end of file +} From 1ce02049aa3b52f7ebfd2516c965b8aebd327c4d Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 25 Nov 2019 09:38:49 -0600 Subject: [PATCH 050/128] Swap renovate codeowners with assignee configuration (#48987) * Swap renovate codeowners with assignee configuration * remove : --- .github/CODEOWNERS | 1 - renovate.json5 | 3 +++ src/dev/renovate/config.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bd73e60d1c914..4e2abd5a3db1c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -45,7 +45,6 @@ /x-pack/test/functional/services/transform.ts @elastic/ml-ui # Operations -/renovate.json5 @elastic/kibana-operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations /src/optimize/ @elastic/kibana-operations diff --git a/renovate.json5 b/renovate.json5 index aefbc61e8dc12..3886715618e99 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -21,6 +21,7 @@ ], labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', @@ -28,6 +29,7 @@ major: { labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', @@ -228,6 +230,7 @@ ], labels: [ 'release_note:skip', + 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0', diff --git a/src/dev/renovate/config.ts b/src/dev/renovate/config.ts index 7e62059f5059a..6acbbaa4d5255 100644 --- a/src/dev/renovate/config.ts +++ b/src/dev/renovate/config.ts @@ -21,7 +21,7 @@ import { RENOVATE_PACKAGE_GROUPS } from './package_groups'; import { PACKAGE_GLOBS } from './package_globs'; import { wordRegExp, maybeFlatMap, maybeMap, getTypePackageName } from './utils'; -const DEFAULT_LABELS = ['release_note:skip', 'renovate', 'v8.0.0', 'v7.6.0']; +const DEFAULT_LABELS = ['release_note:skip', 'Team:Operations', 'renovate', 'v8.0.0', 'v7.6.0']; export const RENOVATE_CONFIG = { extends: ['config:base'], From 0607032ba5b6df3341d98cf66043c3ac71d88ea7 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 25 Nov 2019 17:56:49 +0200 Subject: [PATCH 051/128] =?UTF-8?q?Move=20FilterBar=20component=20?= =?UTF-8?q?=E2=87=92=20NP=20(#51178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Moved filter bar * Fixed import * Fixed search bar test * change css import path * fix imports from data/public --- src/core/MIGRATION.md | 4 +- .../apply_filter_popover_content.tsx | 8 +++- .../public/filter/filter_bar/filter_bar.less | 0 .../core_plugins/data/public/filter/index.tsx | 2 - .../core_plugins/data/public/index.scss | 2 +- src/legacy/core_plugins/data/public/index.ts | 2 +- .../search_bar/components/search_bar.test.tsx | 7 ++- .../search_bar/components/search_bar.tsx | 3 +- src/plugins/data/public/index.ts | 2 + .../ui}/filter_bar/_global_filter_group.scss | 0 .../ui}/filter_bar/_global_filter_item.scss | 0 .../data/public/ui}/filter_bar/_index.scss | 0 .../public/ui}/filter_bar/_variables.scss | 0 .../data/public/ui}/filter_bar/filter_bar.tsx | 4 +- .../filter_editor/_filter_editor.scss | 0 .../ui}/filter_bar/filter_editor/_index.scss | 0 .../filter_editor/generic_combo_box.tsx | 0 .../ui}/filter_bar/filter_editor/index.tsx | 7 +-- .../__snapshots__/filter_label.test.js.snap | 0 .../lib/filter_editor_utils.test.ts | 43 +++++++------------ .../filter_editor/lib/filter_editor_utils.ts | 9 +--- .../filter_editor/lib/filter_label.test.js | 5 +-- .../filter_editor/lib/filter_label.tsx | 2 +- .../filter_editor/lib/filter_operators.ts | 2 +- .../filter_editor/phrase_suggestor.tsx | 11 +---- .../filter_editor/phrase_value_input.tsx | 2 +- .../filter_editor/phrases_values_input.tsx | 2 +- .../filter_editor/range_value_input.tsx | 4 +- .../filter_editor/value_input_type.tsx | 0 .../public/ui}/filter_bar/filter_item.tsx | 2 +- .../public/ui}/filter_bar/filter_options.tsx | 0 .../ui}/filter_bar/filter_view/index.tsx | 2 +- .../data/public/ui/filter_bar/index.ts | 21 +++++++++ .../data/public/ui}/index.ts | 2 +- .../legacy/plugins/ml/common/types/fields.ts | 2 +- .../transform/public/app/common/pivot_aggs.ts | 2 +- .../public/app/common/pivot_group_by.ts | 2 +- 37 files changed, 77 insertions(+), 77 deletions(-) delete mode 100644 src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_global_filter_group.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_global_filter_item.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_index.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/_variables.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_bar.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/_filter_editor.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/_index.scss (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/generic_combo_box.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/index.tsx (99%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_editor_utils.test.ts (79%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_editor_utils.ts (93%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_label.test.js (88%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_label.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/lib/filter_operators.ts (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrase_suggestor.tsx (91%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrase_value_input.tsx (97%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/phrases_values_input.tsx (96%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/range_value_input.tsx (96%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_editor/value_input_type.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_item.tsx (98%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_options.tsx (100%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/filter_bar/filter_view/index.tsx (97%) create mode 100644 src/plugins/data/public/ui/filter_bar/index.ts rename src/{legacy/core_plugins/data/public/filter/filter_bar => plugins/data/public/ui}/index.ts (95%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index f3532cd717ac8..22c96110742e0 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1167,8 +1167,8 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | -| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | +| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | Directive is deprecated. | +| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx index 37d96a51d66d2..954cbca8f054b 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx +++ b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx @@ -31,8 +31,12 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { IndexPattern } from '../../index_patterns'; -import { FilterLabel } from '../filter_bar/filter_editor/lib/filter_label'; -import { mapAndFlattenFilters, esFilters, utils } from '../../../../../../plugins/data/public'; +import { + mapAndFlattenFilters, + esFilters, + utils, + FilterLabel, +} from '../../../../../../plugins/data/public'; interface Props { filters: esFilters.Filter[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.less deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/legacy/core_plugins/data/public/filter/index.tsx index 005c4904a4f39..e48a18fc53a76 100644 --- a/src/legacy/core_plugins/data/public/filter/index.tsx +++ b/src/legacy/core_plugins/data/public/filter/index.tsx @@ -17,6 +17,4 @@ * under the License. */ -export { FilterBar } from './filter_bar'; - export { ApplyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index.scss b/src/legacy/core_plugins/data/public/index.scss index 14274d27c13ee..913141666c7b9 100644 --- a/src/legacy/core_plugins/data/public/index.scss +++ b/src/legacy/core_plugins/data/public/index.scss @@ -2,6 +2,6 @@ @import './query/query_bar/index'; -@import './filter/filter_bar/index'; +@import 'src/plugins/data/public/ui/filter_bar/index'; @import './search/search_bar/index'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index ffce162cadde4..b33aef75e6756 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -29,7 +29,7 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart }; -export { FilterBar, ApplyFiltersPopover } from './filter'; +export { ApplyFiltersPopover } from './filter'; export { Field, FieldType, diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 44637365247fb..0ca9482fefa30 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -35,9 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('../../../../../data/public', () => { +jest.mock('../../../../../../../plugins/data/public', () => { return { FilterBar: () =>
    , + }; +}); + +jest.mock('../../../../../data/public', () => { + return { QueryBarInput: () =>
    , }; }); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index c8b76c9cda99d..6a1ef77a56653 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -24,7 +24,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; -import { IndexPattern, FilterBar } from '../../../../../data/public'; +import { IndexPattern } from '../../../../../data/public'; import { QueryBarTopRow } from '../../../query'; import { SavedQuery, SavedQueryAttributes } from '../index'; import { SavedQueryMeta, SaveQueryForm } from './saved_query_management/save_query_form'; @@ -41,6 +41,7 @@ import { Query, esFilters, TimeHistoryContract, + FilterBar, } from '../../../../../../../plugins/data/public'; interface SearchBarInjectedDeps { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 6a2df6a61d136..ace0b44378b45 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -36,3 +36,5 @@ export * from './types'; export { IRequestTypesMap, IResponseTypesMap } from './search'; export * from './search'; export * from './query'; + +export * from './ui'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_group.scss rename to src/plugins/data/public/ui/filter_bar/_global_filter_group.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_global_filter_item.scss rename to src/plugins/data/public/ui/filter_bar/_global_filter_item.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss b/src/plugins/data/public/ui/filter_bar/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss rename to src/plugins/data/public/ui/filter_bar/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_variables.scss b/src/plugins/data/public/ui/filter_bar/_variables.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/_variables.scss rename to src/plugins/data/public/ui/filter_bar/_variables.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx rename to src/plugins/data/public/ui/filter_bar/filter_bar.tsx index e80bffb5e3c68..2f1b1f8588eb9 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -25,8 +25,8 @@ import React, { useState } from 'react'; import { FilterEditor } from './filter_editor'; import { FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; -import { useKibana } from '../../../../../../plugins/kibana_react/public'; -import { IIndexPattern, esFilters } from '../../../../../../plugins/data/public'; +import { useKibana } from '../../../../kibana_react/public'; +import { IIndexPattern, esFilters } from '../..'; interface Props { filters: esFilters.Filter[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_filter_editor.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss rename to src/plugins/data/public/ui/filter_bar/filter_editor/_filter_editor.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss rename to src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/generic_combo_box.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx similarity index 99% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 4f9424f30f516..12da4cbab02da 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -48,12 +48,7 @@ import { Operator } from './lib/filter_operators'; import { PhraseValueInput } from './phrase_value_input'; import { PhrasesValuesInput } from './phrases_values_input'; import { RangeValueInput } from './range_value_input'; -import { - esFilters, - utils, - IIndexPattern, - IFieldType, -} from '../../../../../../../plugins/data/public'; +import { esFilters, utils, IIndexPattern, IFieldType } from '../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/__snapshots__/filter_label.test.js.snap diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts similarity index 79% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index 6dc9bc2300e04..2cc7f16cfe261 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -17,7 +17,6 @@ * under the License. */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ import { existsFilter, phraseFilter, @@ -25,8 +24,8 @@ import { rangeFilter, stubIndexPattern, stubFields, -} from '../../../../../../../../plugins/data/public/stubs'; -import { IndexPattern, Field } from '../../../../index'; +} from '../../../../stubs'; +import { esFilters } from '../../../../index'; import { getFieldFromFilter, getFilterableFields, @@ -37,17 +36,12 @@ import { import { existsOperator, isBetweenOperator, isOneOfOperator, isOperator } from './filter_operators'; -import { esFilters } from '../../../../../../../../plugins/data/public'; - jest.mock('ui/new_platform'); -const mockedFields = stubFields as Field[]; -const mockedIndexPattern = stubIndexPattern as IndexPattern; - describe('Filter editor utils', () => { describe('getFieldFromFilter', () => { it('should return the field from the filter', () => { - const field = getFieldFromFilter(phraseFilter, mockedIndexPattern); + const field = getFieldFromFilter(phraseFilter, stubIndexPattern); expect(field).not.toBeUndefined(); expect(field && field.name).toBe(phraseFilter.meta.key); }); @@ -117,12 +111,12 @@ describe('Filter editor utils', () => { describe('getFilterableFields', () => { it('returns the list of fields from the given index pattern', () => { - const fieldOptions = getFilterableFields(mockedIndexPattern); + const fieldOptions = getFilterableFields(stubIndexPattern); expect(fieldOptions.length).toBeGreaterThan(0); }); it('limits the fields to the filterable fields', () => { - const fieldOptions = getFilterableFields(mockedIndexPattern); + const fieldOptions = getFilterableFields(stubIndexPattern); const nonFilterableFields = fieldOptions.filter(field => !field.filterable); expect(nonFilterableFields.length).toBe(0); }); @@ -131,14 +125,14 @@ describe('Filter editor utils', () => { describe('getOperatorOptions', () => { it('returns range for number fields', () => { const [field] = stubFields.filter(({ type }) => type === 'number'); - const operatorOptions = getOperatorOptions(field as Field); + const operatorOptions = getOperatorOptions(field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).not.toBeUndefined(); }); it('does not return range for string fields', () => { const [field] = stubFields.filter(({ type }) => type === 'string'); - const operatorOptions = getOperatorOptions(field as Field); + const operatorOptions = getOperatorOptions(field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).toBeUndefined(); }); @@ -146,49 +140,44 @@ describe('Filter editor utils', () => { describe('isFilterValid', () => { it('should return false if index pattern is not provided', () => { - const isValid = isFilterValid(undefined, mockedFields[0], isOperator, 'foo'); + const isValid = isFilterValid(undefined, stubFields[0], isOperator, 'foo'); expect(isValid).toBe(false); }); it('should return false if field is not provided', () => { - const isValid = isFilterValid(mockedIndexPattern, undefined, isOperator, 'foo'); + const isValid = isFilterValid(stubIndexPattern, undefined, isOperator, 'foo'); expect(isValid).toBe(false); }); it('should return false if operator is not provided', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], undefined, 'foo'); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], undefined, 'foo'); expect(isValid).toBe(false); }); it('should return false for phrases filter without phrases', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, []); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isOneOfOperator, []); expect(isValid).toBe(false); }); it('should return true for phrases filter with phrases', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isOneOfOperator, ['foo']); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isOneOfOperator, ['foo']); expect(isValid).toBe(true); }); it('should return false for range filter without range', () => { - const isValid = isFilterValid( - mockedIndexPattern, - mockedFields[0], - isBetweenOperator, - undefined - ); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, undefined); expect(isValid).toBe(false); }); it('should return true for range filter with from', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, { from: 'foo', }); expect(isValid).toBe(true); }); it('should return true for range filter with from/to', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], isBetweenOperator, { + const isValid = isFilterValid(stubIndexPattern, stubFields[0], isBetweenOperator, { from: 'foo', too: 'goo', }); @@ -196,7 +185,7 @@ describe('Filter editor utils', () => { }); it('should return true for exists filter without params', () => { - const isValid = isFilterValid(mockedIndexPattern, mockedFields[0], existsOperator); + const isValid = isFilterValid(stubIndexPattern, stubFields[0], existsOperator); expect(isValid).toBe(true); }); }); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts similarity index 93% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts index e4487af42beaf..422ffb162125d 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -18,14 +18,9 @@ */ import dateMath from '@elastic/datemath'; -import { Ipv4Address } from '../../../../../../../../plugins/kibana_utils/public'; +import { Ipv4Address } from '../../../../../../kibana_utils/public'; import { FILTER_OPERATORS, Operator } from './filter_operators'; -import { - esFilters, - IIndexPattern, - IFieldType, - isFilterable, -} from '../../../../../../../../plugins/data/public'; +import { esFilters, IIndexPattern, IFieldType, isFilterable } from '../../../..'; export function getFieldFromFilter(filter: esFilters.FieldFilter, indexPattern: IIndexPattern) { return indexPattern.fields.find(field => field.name === filter.meta.key); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js similarity index 88% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js index 0f45a33a79ebb..3eb46645522e1 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.test.js +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.js @@ -20,10 +20,7 @@ import React from 'react'; import { FilterLabel } from './filter_label'; import { shallow } from 'enzyme'; - -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import { phraseFilter } from '../../../../../../../../plugins/data/public/stubs'; -/* eslint-enable @kbn/eslint/no-restricted-paths */ +import { phraseFilter } from '../../../../stubs'; test('alias', () => { const filter = { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index 1b4bdb881116b..49a0d6f2ab3bd 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -21,7 +21,7 @@ import React, { Fragment } from 'react'; import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { existsOperator, isOneOfOperator } from './filter_operators'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters } from '../../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts rename to src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts index a3da03db71d6e..bb15cffa67b59 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_operators.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters } from '../../../..'; export interface Operator { message: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx similarity index 91% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 092bf8daa8f2e..61290cc16b8a8 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -19,16 +19,9 @@ import { Component } from 'react'; import { debounce } from 'lodash'; -import { - withKibana, - KibanaReactContextValue, -} from '../../../../../../../plugins/kibana_react/public'; -import { - IDataPluginServices, - IIndexPattern, - IFieldType, -} from '../../../../../../../plugins/data/public'; +import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; +import { IDataPluginServices, IIndexPattern, IFieldType } from '../../..'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx index 7ef51f88ba57e..b16994cb0057b 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_value_input.tsx @@ -24,7 +24,7 @@ import React from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; -import { withKibana } from '../../../../../../../plugins/kibana_react/public'; +import { withKibana } from '../../../../../kibana_react/public'; interface Props extends PhraseSuggestorProps { value?: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx index f3b30e2ad5fd9..aa76684239b63 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrases_values_input.tsx @@ -23,7 +23,7 @@ import { uniq } from 'lodash'; import React from 'react'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; -import { withKibana } from '../../../../../../../plugins/kibana_react/public'; +import { withKibana } from '../../../../../kibana_react/public'; interface Props extends PhraseSuggestorProps { values?: string[]; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx index 3c39a770377a0..65b842f0bd4aa 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/range_value_input.tsx @@ -21,8 +21,8 @@ import { EuiIcon, EuiLink, EuiFormHelpText, EuiFormControlLayoutDelimited } from import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import React from 'react'; -import { useKibana } from '../../../../../../../plugins/kibana_react/public'; -import { IFieldType } from '../../../../../../../plugins/data/public'; +import { useKibana } from '../../../../../kibana_react/public'; +import { IFieldType } from '../../..'; import { ValueInputType } from './value_input_type'; interface RangeParams { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/value_input_type.tsx rename to src/plugins/data/public/ui/filter_bar/filter_editor/value_input_type.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx similarity index 98% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx rename to src/plugins/data/public/ui/filter_bar/filter_item.tsx index 27406232dd5d3..4ef0b2740e5fa 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -24,7 +24,7 @@ import React, { Component } from 'react'; import { UiSettingsClientContract } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; -import { esFilters, utils, IIndexPattern } from '../../../../../../plugins/data/public'; +import { esFilters, utils, IIndexPattern } from '../..'; interface Props { id: string; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx similarity index 100% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_options.tsx rename to src/plugins/data/public/ui/filter_bar/filter_options.tsx diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx rename to src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 39d4a80cdf540..dd12789d15a9d 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -21,7 +21,7 @@ import { EuiBadge, useInnerText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; import { FilterLabel } from '../filter_editor/lib/filter_label'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../..'; interface Props { filter: esFilters.Filter; diff --git a/src/plugins/data/public/ui/filter_bar/index.ts b/src/plugins/data/public/ui/filter_bar/index.ts new file mode 100644 index 0000000000000..b975317d46630 --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { FilterBar } from './filter_bar'; +export { FilterLabel } from './filter_editor/lib/filter_label'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts b/src/plugins/data/public/ui/index.ts similarity index 95% rename from src/legacy/core_plugins/data/public/filter/filter_bar/index.ts rename to src/plugins/data/public/ui/index.ts index 438d292b9f583..d0aaf2f6aac1c 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { FilterBar } from './filter_bar'; +export * from './filter_bar'; diff --git a/x-pack/legacy/plugins/ml/common/types/fields.ts b/x-pack/legacy/plugins/ml/common/types/fields.ts index f9d9b6b0161e2..9e1b992eec907 100644 --- a/x-pack/legacy/plugins/ml/common/types/fields.ts +++ b/x-pack/legacy/plugins/ml/common/types/fields.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts index adc4bfd1b5918..af55732691bb0 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/pivot_aggs.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; diff --git a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts index 85daad6c7fd52..e6792958ab5d2 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/pivot_group_by.ts @@ -5,7 +5,7 @@ */ import { Dictionary } from '../../../common/types/common'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; import { AggName } from './aggregations'; import { EsFieldName } from './fields'; From c7f8086e114d6d8bd39ab9b05f77e5c123a647cf Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 25 Nov 2019 16:02:53 +0000 Subject: [PATCH 052/128] Removes redundant code for awaiting migrations in Task Manager Thanks to #43433 being merged we no longer need to wait for the migrations to run as they are guaranteed to have run by the time plugin init has completed. This is just a cleanup making it easier to move towards the migration to the Kibana Platform. --- x-pack/legacy/plugins/task_manager/index.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/task_manager/index.ts b/x-pack/legacy/plugins/task_manager/index.ts index 0fda1490de714..0487b003bc1ef 100644 --- a/x-pack/legacy/plugins/task_manager/index.ts +++ b/x-pack/legacy/plugins/task_manager/index.ts @@ -72,16 +72,7 @@ export function taskManager(kibana: any) { } ); this.kbnServer.afterPluginsInit(() => { - (async () => { - // The code block below can't await directly within "afterPluginsInit" - // callback due to circular dependency. The server isn't "ready" until - // this code block finishes. Migrations wait for server to be ready before - // executing. Saved objects repository waits for migrations to finish before - // finishing the request. To avoid this, we'll await within a separate - // function block. - await this.kbnServer.server.kibanaMigrator.runMigrations(); - plugin.start(); - })(); + plugin.start(); }); server.expose(setupContract); }, From 2acc287fc2aca3f2b3552935bb988cf7899bc519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Mon, 25 Nov 2019 17:05:51 +0100 Subject: [PATCH 053/128] [Logs UI] log rate setup index validation (#50008) * Scaffold API endpoint * Implement the API endpoint * Implement API client * Set error messages in `useAnalysisSetupState` * Show validation errors next to the submit button * Check for setup errors regarding the selected indexes * Call validation only once Enrich the `availableIndices` array with validation information to show it later in the form. * Ensure validation runs before showing the indices * Adjust naming conventions - Replace `index_pattern` with `indices`, since it means something different in kibana. - Group validation actions under the `validation` namespace. * Move index error messages to the `InitialConfigurationStep` * Move error messages to the UI layer * Move validation call to `useAnalysisSetupState` * Pass timestamp as a parameter of `useAnalysisSetupState` * Fix regression with the index names in the API response * Use `_field_caps` api * s/timestamp/timestampField/g * Tweak error messages * Move `ValidationIndicesUIError` to `log_analysis_setup_state` * Track validation status It's safer to rely on the state of the promise instead of treating an empty array as "loading" * Handle network errors * Use individual `` elements for the indices This allows to disable individual checkboxes * Pass the whole `validatedIndices` array to the inner objects This will make easier to determine which indeces have errors in the checkbox list itself and simplify the state we keep track of. * Disable indices with errors Show a tooltip above the disabled index to explain why it cannot be selected. * Pass indices to the API as an array * Show overlay while the validation loads * Wrap tooltips on a `block` element Prevents the checkboxes from collapsing on the same line * Use the right dependencies for `useEffect => validateIndices()` * Restore formatter function name * Simplify mapping of selected indices to errors * s/checked/isSelected/g * Make errors field-generic * Allow multiple errors per index * Simplify code a bit --- .../common/http_api/log_analysis/index.ts | 1 + .../http_api/log_analysis/validation/index.ts | 7 + .../log_analysis/validation/indices.ts | 51 ++++++ x-pack/legacy/plugins/infra/package.json | 2 +- .../components/loading_overlay_wrapper.tsx | 1 + .../api/index_patterns_validate.ts | 33 ++++ .../logs/log_analysis/log_analysis_jobs.tsx | 5 +- .../log_analysis/log_analysis_setup_state.tsx | 107 ++++++++++--- .../pages/logs/analysis/page_content.tsx | 2 + .../logs/analysis/page_setup_content.tsx | 3 + .../analysis_setup_indices_form.tsx | 148 ++++++++++++------ .../initial_configuration_step.tsx | 92 ++++++++--- .../setup/process_step/process_step.tsx | 4 +- .../pages/logs/analysis/setup/setup_steps.tsx | 13 +- .../plugins/infra/server/infra_server.ts | 6 +- .../lib/adapters/framework/adapter_types.ts | 25 ++- .../infra/server/routes/log_analysis/index.ts | 1 + .../log_analysis/index_patterns/index.ts | 7 + .../log_analysis/index_patterns/validate.ts | 83 ++++++++++ 19 files changed, 487 insertions(+), 104 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts create mode 100644 x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts create mode 100644 x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts create mode 100644 x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts index 38684cb22e237..378e32cb3582c 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/index.ts @@ -5,3 +5,4 @@ */ export * from './results'; +export * from './validation'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts new file mode 100644 index 0000000000000..727faca69298e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './indices'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts new file mode 100644 index 0000000000000..62d81dc136853 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices'; + +/** + * Request types + */ +export const validationIndicesRequestPayloadRT = rt.type({ + data: rt.type({ + timestampField: rt.string, + indices: rt.array(rt.string), + }), +}); + +export type ValidationIndicesRequestPayload = rt.TypeOf; + +/** + * Response types + * */ +export const validationIndicesErrorRT = rt.union([ + rt.type({ + error: rt.literal('INDEX_NOT_FOUND'), + index: rt.string, + }), + rt.type({ + error: rt.literal('FIELD_NOT_FOUND'), + index: rt.string, + field: rt.string, + }), + rt.type({ + error: rt.literal('FIELD_NOT_VALID'), + index: rt.string, + field: rt.string, + }), +]); + +export type ValidationIndicesError = rt.TypeOf; + +export const validationIndicesResponsePayloadRT = rt.type({ + data: rt.type({ + errors: rt.array(validationIndicesErrorRT), + }), +}); + +export type ValidationIndicesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/package.json b/x-pack/legacy/plugins/infra/package.json index 63812bb2da513..7aa8cb9b5269a 100644 --- a/x-pack/legacy/plugins/infra/package.json +++ b/x-pack/legacy/plugins/infra/package.json @@ -16,4 +16,4 @@ "boom": "7.3.0", "lodash": "^4.17.15" } -} \ No newline at end of file +} diff --git a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx index a99b265fc3ea9..5df1fc07e83b9 100644 --- a/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx +++ b/x-pack/legacy/plugins/infra/public/components/loading_overlay_wrapper.tsx @@ -40,4 +40,5 @@ const OverlayDiv = euiStyled.div` position: absolute; top: 0; width: 100%; + z-index: ${props => props.theme.eui.euiZLevel1}; `; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts new file mode 100644 index 0000000000000..440ee10e4223d --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { identity } from 'fp-ts/lib/function'; +import { kfetch } from 'ui/kfetch'; + +import { + LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validationIndicesRequestPayloadRT, + validationIndicesResponsePayloadRT, +} from '../../../../../common/http_api'; + +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; + +export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => { + const response = await kfetch({ + method: 'POST', + pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + body: JSON.stringify( + validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } }) + ), + }); + + return pipe( + validationIndicesResponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx index 163f0e39d1228..0f386f416b866 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx @@ -94,8 +94,8 @@ export const useLogAnalysisJobs = ({ dispatch({ type: 'fetchingJobStatuses' }); return await callJobsSummaryAPI(spaceId, sourceId); }, - onResolve: response => { - dispatch({ type: 'fetchedJobStatuses', payload: response, spaceId, sourceId }); + onResolve: jobResponse => { + dispatch({ type: 'fetchedJobStatuses', payload: jobResponse, spaceId, sourceId }); }, onReject: err => { dispatch({ type: 'failedFetchingJobStatuses' }); @@ -158,6 +158,7 @@ export const useLogAnalysisJobs = ({ setup: setupMlModule, setupMlModuleRequest, setupStatus: statusState.setupStatus, + timestampField: timeField, viewSetupForReconfiguration, viewSetupForUpdate, viewResults, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index 7942657018455..c965c50bedccc 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import { isExampleDataIndex } from '../../../../common/log_analysis'; +import { + ValidationIndicesError, + ValidationIndicesResponsePayload, +} from '../../../../common/http_api'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callIndexPatternsValidate } from './api/index_patterns_validate'; type SetupHandler = ( indices: string[], @@ -14,42 +20,75 @@ type SetupHandler = ( endTime: number | undefined ) => void; +export type ValidationIndicesUIError = + | ValidationIndicesError + | { error: 'NETWORK_ERROR' } + | { error: 'TOO_FEW_SELECTED_INDICES' }; + +export interface ValidatedIndex { + index: string; + errors: ValidationIndicesError[]; + isSelected: boolean; +} + interface AnalysisSetupStateArguments { availableIndices: string[]; cleanupAndSetupModule: SetupHandler; setupModule: SetupHandler; + timestampField: string; } -type IndicesSelection = Record; - -type ValidationErrors = 'TOO_FEW_SELECTED_INDICES'; - const fourWeeksInMs = 86400000 * 7 * 4; export const useAnalysisSetupState = ({ availableIndices, cleanupAndSetupModule, setupModule, + timestampField, }: AnalysisSetupStateArguments) => { const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); - const [selectedIndices, setSelectedIndices] = useState( - availableIndices.reduce( - (indexMap, indexName) => ({ - ...indexMap, - [indexName]: !(availableIndices.length > 1 && isExampleDataIndex(indexName)), - }), - {} - ) + // Prepare the validation + const [validatedIndices, setValidatedIndices] = useState( + availableIndices.map(index => ({ + index, + errors: [], + isSelected: false, + })) + ); + const [validateIndicesRequest, validateIndices] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callIndexPatternsValidate(timestampField, availableIndices); + }, + onResolve: ({ data }: ValidationIndicesResponsePayload) => { + setValidatedIndices( + availableIndices.map(index => { + const errors = data.errors.filter(error => error.index === index); + return { + index, + errors, + isSelected: errors.length === 0 && !isExampleDataIndex(index), + }; + }) + ); + }, + onReject: () => { + setValidatedIndices([]); + }, + }, + [availableIndices, timestampField] ); + useEffect(() => { + validateIndices(); + }, [validateIndices]); + const selectedIndexNames = useMemo( - () => - Object.entries(selectedIndices) - .filter(([_indexName, isSelected]) => isSelected) - .map(([indexName]) => indexName), - [selectedIndices] + () => validatedIndices.filter(i => i.isSelected).map(i => i.index), + [validatedIndices] ); const setup = useCallback(() => { @@ -60,24 +99,42 @@ export const useAnalysisSetupState = ({ return cleanupAndSetupModule(selectedIndexNames, startTime, endTime); }, [cleanupAndSetupModule, selectedIndexNames, startTime, endTime]); - const validationErrors: ValidationErrors[] = useMemo( + const isValidating = useMemo( () => - Object.values(selectedIndices).some(isSelected => isSelected) - ? [] - : ['TOO_FEW_SELECTED_INDICES' as const], - [selectedIndices] + validateIndicesRequest.state === 'pending' || + validateIndicesRequest.state === 'uninitialized', + [validateIndicesRequest.state] ); + const validationErrors = useMemo(() => { + if (isValidating) { + return []; + } + + if (validateIndicesRequest.state === 'rejected') { + return [{ error: 'NETWORK_ERROR' }]; + } + + if (selectedIndexNames.length === 0) { + return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; + } + + return validatedIndices.reduce((errors, index) => { + return selectedIndexNames.includes(index.index) ? errors.concat(index.errors) : errors; + }, []); + }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); + return { cleanupAndSetup, endTime, + isValidating, selectedIndexNames, - selectedIndices, setEndTime, - setSelectedIndices, setStartTime, setup, startTime, + validatedIndices, + setValidatedIndices, validationErrors, }; }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx index 04d7520c0ca88..f0a26eae25ecb 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx @@ -27,6 +27,7 @@ export const AnalysisPageContent = () => { lastSetupErrorMessages, setup, setupStatus, + timestampField, viewResults, } = useContext(LogAnalysisJobs.Context); @@ -61,6 +62,7 @@ export const AnalysisPageContent = () => { errorMessages={lastSetupErrorMessages} setup={setup} setupStatus={setupStatus} + timestampField={timestampField} viewResults={viewResults} /> ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx index 097cccf5dca33..7ae174c4a7899 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx @@ -34,6 +34,7 @@ interface AnalysisSetupContentProps { errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; + timestampField: string; viewResults: () => void; } @@ -43,6 +44,7 @@ export const AnalysisSetupContent: React.FunctionComponent { useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' }); @@ -82,6 +84,7 @@ export const AnalysisSetupContent: React.FunctionComponent diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx index defcefd69a7ab..585a65b9ad1c8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -4,37 +4,58 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCheckboxGroup, EuiCode, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiCode, EuiDescribedFormGroup, EuiFormRow, EuiCheckbox, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; - -export type IndicesSelection = Record; - -export type IndicesValidationError = 'TOO_FEW_SELECTED_INDICES'; +import { + ValidatedIndex, + ValidationIndicesUIError, +} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state'; +import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; export const AnalysisSetupIndicesForm: React.FunctionComponent<{ - indices: IndicesSelection; - onChangeSelectedIndices: (selectedIndices: IndicesSelection) => void; - validationErrors?: IndicesValidationError[]; -}> = ({ indices, onChangeSelectedIndices, validationErrors = [] }) => { + indices: ValidatedIndex[]; + isValidating: boolean; + onChangeSelectedIndices: (selectedIndices: ValidatedIndex[]) => void; + valid: boolean; +}> = ({ indices, isValidating, onChangeSelectedIndices, valid }) => { + const handleCheckboxChange = useCallback( + (event: React.ChangeEvent) => { + onChangeSelectedIndices( + indices.map(index => { + const checkbox = event.currentTarget; + return index.index === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; + }) + ); + }, + [indices, onChangeSelectedIndices] + ); + const choices = useMemo( () => - Object.keys(indices).map(indexName => ({ - id: indexName, - label: {indexName}, - })), - [indices] - ); + indices.map(index => { + const validIndex = index.errors.length === 0; + const checkbox = ( + {index.index}} + onChange={handleCheckboxChange} + checked={index.isSelected} + disabled={!validIndex} + /> + ); - const handleCheckboxGroupChange = useCallback( - indexName => { - onChangeSelectedIndices({ - ...indices, - [indexName]: !indices[indexName], - }); - }, - [indices, onChangeSelectedIndices] + return validIndex ? ( + checkbox + ) : ( +
    + {checkbox} +
    + ); + }), + [indices] ); return ( @@ -53,20 +74,17 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ /> } > - 0} - label={indicesSelectionLabel} - labelType="legend" - > - - + + + <>{choices} + + ); }; @@ -75,14 +93,50 @@ const indicesSelectionLabel = i18n.translate('xpack.infra.analysisSetup.indicesS defaultMessage: 'Indices', }); -const formatValidationError = (validationError: IndicesValidationError) => { - switch (validationError) { - case 'TOO_FEW_SELECTED_INDICES': - return i18n.translate( - 'xpack.infra.analysisSetup.indicesSelectionTooFewSelectedIndicesDescription', - { - defaultMessage: 'Select at least one index name.', - } - ); - } +const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { + return errors.map(error => { + switch (error.error) { + case 'INDEX_NOT_FOUND': + return ( +

    + {error.index} }} + /> +

    + ); + + case 'FIELD_NOT_FOUND': + return ( +

    + {error.index}, + field: {error.field}, + }} + /> +

    + ); + + case 'FIELD_NOT_VALID': + return ( +

    + {error.index}, + field: {error.field}, + }} + /> +

    + ); + + default: + return ''; + } + }); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx index 929fba26f2323..3b5497fb91864 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx @@ -4,24 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiForm } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import { EuiSpacer, EuiForm, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; -import { - AnalysisSetupIndicesForm, - IndicesSelection, - IndicesValidationError, -} from './analysis_setup_indices_form'; +import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form'; import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; +import { + ValidatedIndex, + ValidationIndicesUIError, +} from '../../../../../containers/logs/log_analysis/log_analysis_setup_state'; interface InitialConfigurationStepProps { setStartTime: (startTime: number | undefined) => void; setEndTime: (endTime: number | undefined) => void; startTime: number | undefined; endTime: number | undefined; - selectedIndices: IndicesSelection; - setSelectedIndices: (selectedIndices: IndicesSelection) => void; - validationErrors?: IndicesValidationError[]; + isValidating: boolean; + validatedIndices: ValidatedIndex[]; + setValidatedIndices: (selectedIndices: ValidatedIndex[]) => void; + validationErrors?: ValidationIndicesUIError[]; } export const InitialConfigurationStep: React.FunctionComponent = ({ @@ -29,16 +32,11 @@ export const InitialConfigurationStep: React.FunctionComponent { - const indicesFormValidationErrors = useMemo( - () => - validationErrors.filter(validationError => validationError === 'TOO_FEW_SELECTED_INDICES'), - [validationErrors] - ); - return ( <> @@ -50,11 +48,63 @@ export const InitialConfigurationStep: React.FunctionComponent + + ); }; + +const errorCalloutTitle = i18n.translate( + 'xpack.infra.analysisSetup.steps.initialConfigurationStep.errorCalloutTitle', + { + defaultMessage: 'Your index configuration is not valid', + } +); + +const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ errors }) => { + if (errors.length === 0) { + return null; + } + + return ( + <> + +
      + {errors.map((error, i) => ( +
    • {formatValidationError(error)}
    • + ))} +
    +
    + + + ); +}; + +const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode => { + switch (error.error) { + case 'NETWORK_ERROR': + return ( + + ); + + case 'TOO_FEW_SELECTED_INDICES': + return ( + + ); + + default: + return ''; + } +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx index 27fc8a83bc086..978e45e26b733 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx @@ -60,8 +60,8 @@ export const ProcessStep: React.FunctionComponent = ({ defaultMessage="Something went wrong creating the necessary ML jobs. Please ensure all selected log indices exist." /> - {errorMessages.map(errorMessage => ( - + {errorMessages.map((errorMessage, i) => ( + {errorMessage} ))} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx index aebb44d4c9372..4643516e73fac 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx @@ -25,6 +25,7 @@ interface AnalysisSetupStepsProps { errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; + timestampField: string; viewResults: () => void; } @@ -34,6 +35,7 @@ export const AnalysisSetupSteps: React.FunctionComponent { const { @@ -43,13 +45,15 @@ export const AnalysisSetupSteps: React.FunctionComponent ), diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index 98536f4c85d36..0093a6c21af57 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -13,7 +13,10 @@ import { createSnapshotResolvers } from './graphql/snapshot'; import { createSourceStatusResolvers } from './graphql/source_status'; import { createSourcesResolvers } from './graphql/sources'; import { InfraBackendLibs } from './lib/infra_types'; -import { initLogAnalysisGetLogEntryRateRoute } from './routes/log_analysis'; +import { + initLogAnalysisGetLogEntryRateRoute, + initIndexPatternsValidateRoute, +} from './routes/log_analysis'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; import { initMetadataRoute } from './routes/metadata'; @@ -33,6 +36,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initIpToHostName(libs); initLogAnalysisGetLogEntryRateRoute(libs); + initIndexPatternsValidateRoute(libs); initMetricExplorerRoute(libs); initMetadataRoute(libs); }; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index f0d26e5f5869f..63fded49d8222 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -45,7 +45,12 @@ export interface InfraBackendFrameworkAdapter { ): Promise; callWithRequest( req: InfraFrameworkRequest, - method: 'indices.getAlias' | 'indices.get', + method: 'indices.getAlias', + options?: object + ): Promise; + callWithRequest( + req: InfraFrameworkRequest, + method: 'indices.get', options?: object ): Promise; callWithRequest( @@ -137,14 +142,32 @@ export interface InfraDatabaseMultiResponse extends InfraDatab } export interface InfraDatabaseFieldCapsResponse extends InfraDatabaseResponse { + indices: string[]; fields: InfraFieldsResponse; } +export interface InfraDatabaseGetIndicesAliasResponse { + [indexName: string]: { + aliases: { + [aliasName: string]: any; + }; + }; +} + export interface InfraDatabaseGetIndicesResponse { [indexName: string]: { aliases: { [aliasName: string]: any; }; + mappings: { + _meta: object; + dynamic_templates: any[]; + date_detection: boolean; + properties: { + [fieldName: string]: any; + }; + }; + settings: { index: object }; }; } diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts index 38684cb22e237..7364d167efe47 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts @@ -5,3 +5,4 @@ */ export * from './results'; +export * from './index_patterns'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts new file mode 100644 index 0000000000000..a85e119e7318a --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './validate'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts new file mode 100644 index 0000000000000..0a369adb7ca29 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Boom from 'boom'; + +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { + LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validationIndicesRequestPayloadRT, + validationIndicesResponsePayloadRT, + ValidationIndicesError, +} from '../../../../common/http_api'; + +import { throwErrors } from '../../../../common/runtime_types'; + +const partitionField = 'event.dataset'; + +export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => { + framework.registerRoute({ + method: 'POST', + path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + handler: async (req, res) => { + const payload = pipe( + validationIndicesRequestPayloadRT.decode(req.payload), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { timestampField, indices } = payload.data; + const errors: ValidationIndicesError[] = []; + + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async index => { + const fieldCaps = await framework.callWithRequest(req, 'fieldCaps', { + index, + fields: `${timestampField},${partitionField}`, + }); + + if (fieldCaps.indices.length === 0) { + errors.push({ + error: 'INDEX_NOT_FOUND', + index, + }); + return; + } + + ([ + [timestampField, 'date'], + [partitionField, 'keyword'], + ] as const).forEach(([field, fieldType]) => { + const fieldMetadata = fieldCaps.fields[field]; + + if (fieldMetadata === undefined) { + errors.push({ + error: 'FIELD_NOT_FOUND', + index, + field, + }); + } else { + const fieldTypes = Object.keys(fieldMetadata); + + if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + errors.push({ + error: `FIELD_NOT_VALID`, + index, + field, + }); + } + } + }); + }) + ); + + return res.response(validationIndicesResponsePayloadRT.encode({ data: { errors } })); + }, + }); +}; From b136bfdb82266e1f1265e70658f01788fdda4aeb Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 25 Nov 2019 10:19:24 -0700 Subject: [PATCH 054/128] [File upload][Maps] NP migration for server & client (#51045) * Use np savedObjectsClient in indexing service * Export File Upload UI via start since it requires initialization * Pass services through top level react component props * Handle basePath ref and 'kbn-version' for requests * Bulk of logic for removing hapi server dependencies for server app * Use request obj subset of original request * Move startup logic over to server plugin file and call from index.js * Update server tests * Clean up * Remove old makeUsageCollector export statement * initServicesAndConstants in the start method instead of in the react component * Review feedback --- .../common/constants/file_import.ts | 2 + x-pack/legacy/plugins/file_upload/index.js | 29 ++++++++--- .../legacy/plugins/file_upload/mappings.json | 9 ---- x-pack/legacy/plugins/file_upload/mappings.ts | 15 ++++++ .../public/components/json_import_progress.js | 4 +- .../file_upload/public/{index.js => index.ts} | 6 ++- .../file_upload/public/kibana_services.js | 13 +++++ .../plugins/file_upload/public/legacy.ts | 12 +++++ .../plugins/file_upload/public/plugin.ts | 33 ++++++++++++ .../file_upload/public/util/http_service.js | 4 +- .../public/util/indexing_service.js | 24 ++++----- .../call_with_internal_user_factory.d.ts | 4 +- .../client/call_with_internal_user_factory.js | 9 ++-- .../call_with_internal_user_factory.test.ts | 22 +++----- .../client/call_with_request_factory.js | 8 +-- .../plugins/file_upload/server/plugin.js | 36 +++++++++++++ .../file_upload/server/routes/file_upload.js | 50 +++++++++++-------- .../file_upload/server/telemetry/index.ts | 1 - .../server/telemetry/make_usage_collector.ts | 27 ---------- .../server/telemetry/telemetry.test.ts | 22 ++++---- .../file_upload/server/telemetry/telemetry.ts | 33 ++++++++---- .../create_client_file_source_editor.js | 4 +- 22 files changed, 229 insertions(+), 138 deletions(-) delete mode 100644 x-pack/legacy/plugins/file_upload/mappings.json create mode 100644 x-pack/legacy/plugins/file_upload/mappings.ts rename x-pack/legacy/plugins/file_upload/public/{index.js => index.ts} (69%) create mode 100644 x-pack/legacy/plugins/file_upload/public/legacy.ts create mode 100644 x-pack/legacy/plugins/file_upload/public/plugin.ts create mode 100644 x-pack/legacy/plugins/file_upload/server/plugin.js delete mode 100644 x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts diff --git a/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts b/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts index 1c82c2b6237e1..0770899af5393 100644 --- a/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts +++ b/x-pack/legacy/plugins/file_upload/common/constants/file_import.ts @@ -16,3 +16,5 @@ export const ES_GEO_FIELD_TYPE = { GEO_POINT: 'geo_point', GEO_SHAPE: 'geo_shape', }; + +export const DEFAULT_KBN_VERSION = 'kbnVersion'; diff --git a/x-pack/legacy/plugins/file_upload/index.js b/x-pack/legacy/plugins/file_upload/index.js index 24907082adb2c..37d4ad80fa2ca 100644 --- a/x-pack/legacy/plugins/file_upload/index.js +++ b/x-pack/legacy/plugins/file_upload/index.js @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { fileUploadRoutes } from './server/routes/file_upload'; -import { makeUsageCollector } from './server/telemetry/'; -import mappings from './mappings'; +import { FileUploadPlugin } from './server/plugin'; +import { mappings } from './mappings'; export const fileUpload = kibana => { return new kibana.Plugin({ @@ -23,11 +21,26 @@ export const fileUpload = kibana => { }, init(server) { - const { xpack_main: xpackMainPlugin } = server.plugins; + const coreSetup = server.newPlatform.setup.core; + const pluginsSetup = {}; - mirrorPluginStatus(xpackMainPlugin, this); - fileUploadRoutes(server); - makeUsageCollector(server); + // legacy dependencies + const __LEGACY = { + route: server.route.bind(server), + plugins: { + elasticsearch: server.plugins.elasticsearch, + }, + savedObjects: { + getSavedObjectsRepository: server.savedObjects.getSavedObjectsRepository + }, + usage: { + collectorSet: { + makeUsageCollector: server.usage.collectorSet.makeUsageCollector + } + } + }; + + new FileUploadPlugin().setup(coreSetup, pluginsSetup, __LEGACY); } }); }; diff --git a/x-pack/legacy/plugins/file_upload/mappings.json b/x-pack/legacy/plugins/file_upload/mappings.json deleted file mode 100644 index addff6308d3f0..0000000000000 --- a/x-pack/legacy/plugins/file_upload/mappings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - } -} diff --git a/x-pack/legacy/plugins/file_upload/mappings.ts b/x-pack/legacy/plugins/file_upload/mappings.ts new file mode 100644 index 0000000000000..70229c7088324 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/mappings.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mappings = { + 'file-upload-telemetry': { + properties: { + filesUploadedTotalCount: { + type: 'long', + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js b/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js index 9e553a536845d..9c6248049d9cf 100644 --- a/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js +++ b/x-pack/legacy/plugins/file_upload/public/components/json_import_progress.js @@ -8,7 +8,7 @@ import React, { Fragment, Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCodeBlock, EuiSpacer, EuiText, EuiTitle, EuiProgress, EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; +import { basePath } from '../kibana_services'; export class JsonImportProgress extends Component { @@ -114,7 +114,7 @@ export class JsonImportProgress extends Component { diff --git a/x-pack/legacy/plugins/file_upload/public/index.js b/x-pack/legacy/plugins/file_upload/public/index.ts similarity index 69% rename from x-pack/legacy/plugins/file_upload/public/index.js rename to x-pack/legacy/plugins/file_upload/public/index.ts index a02b82170f70f..205ceae37d6a1 100644 --- a/x-pack/legacy/plugins/file_upload/public/index.js +++ b/x-pack/legacy/plugins/file_upload/public/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { JsonUploadAndParse } from './components/json_upload_and_parse'; +import { FileUploadPlugin } from './plugin'; + +export function plugin() { + return new FileUploadPlugin(); +} diff --git a/x-pack/legacy/plugins/file_upload/public/kibana_services.js b/x-pack/legacy/plugins/file_upload/public/kibana_services.js index 10a6ae7179bc2..3c00ab5709660 100644 --- a/x-pack/legacy/plugins/file_upload/public/kibana_services.js +++ b/x-pack/legacy/plugins/file_upload/public/kibana_services.js @@ -5,5 +5,18 @@ */ import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { DEFAULT_KBN_VERSION } from '../common/constants/file_import'; export const indexPatternService = data.indexPatterns.indexPatterns; + +export let savedObjectsClient; +export let basePath; +export let apiBasePath; +export let kbnVersion; + +export const initServicesAndConstants = ({ savedObjects, http, injectedMetadata }) => { + savedObjectsClient = savedObjects.client; + basePath = http.basePath.basePath; + apiBasePath = http.basePath.prepend('/api'); + kbnVersion = injectedMetadata.getKibanaVersion(DEFAULT_KBN_VERSION); +}; diff --git a/x-pack/legacy/plugins/file_upload/public/legacy.ts b/x-pack/legacy/plugins/file_upload/public/legacy.ts new file mode 100644 index 0000000000000..719599df3ccbe --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/public/legacy.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { npStart } from 'ui/new_platform'; +import { plugin } from '.'; + +const pluginInstance = plugin(); + +export const start = pluginInstance.start(npStart.core); diff --git a/x-pack/legacy/plugins/file_upload/public/plugin.ts b/x-pack/legacy/plugins/file_upload/public/plugin.ts new file mode 100644 index 0000000000000..cc9ebbfc15b39 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/public/plugin.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreStart } from 'src/core/public'; +// @ts-ignore +import { initResources } from './util/indexing_service'; +// @ts-ignore +import { JsonUploadAndParse } from './components/json_upload_and_parse'; +// @ts-ignore +import { initServicesAndConstants } from './kibana_services'; + +/** + * These are the interfaces with your public contracts. You should export these + * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. + * @public + */ +export type FileUploadPluginSetup = ReturnType; +export type FileUploadPluginStart = ReturnType; + +/** @internal */ +export class FileUploadPlugin implements Plugin { + public setup() {} + + public start(core: CoreStart) { + initServicesAndConstants(core); + return { + JsonUploadAndParse, + }; + } +} diff --git a/x-pack/legacy/plugins/file_upload/public/util/http_service.js b/x-pack/legacy/plugins/file_upload/public/util/http_service.js index 26d46cecb0e51..a744f0f075490 100644 --- a/x-pack/legacy/plugins/file_upload/public/util/http_service.js +++ b/x-pack/legacy/plugins/file_upload/public/util/http_service.js @@ -6,9 +6,9 @@ // service for interacting with the server -import chrome from 'ui/chrome'; import { addSystemApiHeader } from 'ui/system_api'; import { i18n } from '@kbn/i18n'; +import { kbnVersion } from '../kibana_services'; export async function http(options) { if(!(options && options.url)) { @@ -20,7 +20,7 @@ export async function http(options) { const url = options.url || ''; const headers = addSystemApiHeader({ 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), + 'kbn-version': kbnVersion, ...options.headers }); diff --git a/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js b/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js index fd96bd95d4bb7..b40659ec4b513 100644 --- a/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js +++ b/x-pack/legacy/plugins/file_upload/public/util/indexing_service.js @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { http } from './http_service'; -import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import { indexPatternService } from '../kibana_services'; +import { http as httpService } from './http_service'; +import { + indexPatternService, + apiBasePath, + savedObjectsClient +} from '../kibana_services'; import { getGeoJsonIndexingDetails } from './geo_processing'; import { sizeLimitedChunking } from './size_limited_chunking'; +import { i18n } from '@kbn/i18n'; -const basePath = chrome.addBasePath('/api/fileupload'); const fileType = 'json'; export async function indexData(parsedFile, transformDetails, indexName, dataType, appName) { @@ -19,7 +21,6 @@ export async function indexData(parsedFile, transformDetails, indexName, dataTyp throw(i18n.translate('xpack.fileUpload.indexingService.noFileImported', { defaultMessage: 'No file imported.' })); - return; } // Perform any processing required on file prior to indexing @@ -129,8 +130,8 @@ async function writeToIndex(indexingDetails) { ingestPipeline } = indexingDetails; - return await http({ - url: `${basePath}/import${paramString}`, + return await httpService({ + url: `${apiBasePath}/fileupload/import${paramString}`, method: 'POST', data: { index, @@ -223,7 +224,6 @@ export async function createIndexPattern(indexPatternName) { } async function getIndexPatternId(name) { - const savedObjectsClient = chrome.getSavedObjectsClient(); const savedObjectSearch = await savedObjectsClient.find({ type: 'index-pattern', perPage: 1000 }); const indexPatternSavedObjects = savedObjectSearch.savedObjects; @@ -237,9 +237,8 @@ async function getIndexPatternId(name) { } export const getExistingIndexNames = async () => { - const basePath = chrome.addBasePath('/api'); - const indexes = await http({ - url: `${basePath}/index_management/indices`, + const indexes = await httpService({ + url: `${apiBasePath}/index_management/indices`, method: 'GET', }); return indexes @@ -248,7 +247,6 @@ export const getExistingIndexNames = async () => { }; export const getExistingIndexPatternNames = async () => { - const savedObjectsClient = chrome.getSavedObjectsClient(); const indexPatterns = await savedObjectsClient.find({ type: 'index-pattern', fields: ['id', 'title', 'type', 'fields'], diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts index 0b39c81cee6ff..9c1000db8cb56 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; - -export function callWithInternalUserFactory(server: Server): any; +export function callWithInternalUserFactory(elasticsearchPlugin: any): any; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js index dc3131484e75f..f42c3ffb99a5b 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.js @@ -5,16 +5,15 @@ */ - import { once } from 'lodash'; -const _callWithInternalUser = once((server) => { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); +const _callWithInternalUser = once(elasticsearchPlugin => { + const { callWithInternalUser } = elasticsearchPlugin.getCluster('admin'); return callWithInternalUser; }); -export const callWithInternalUserFactory = (server) => { +export const callWithInternalUserFactory = elasticsearchPlugin => { return (...args) => { - return _callWithInternalUser(server)(...args); + return _callWithInternalUser(elasticsearchPlugin)(...args); }; }; diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts index d77541e7d3d6c..04c5013ed8e67 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts @@ -8,25 +8,15 @@ import { callWithInternalUserFactory } from './call_with_internal_user_factory'; describe('call_with_internal_user_factory', () => { describe('callWithInternalUserFactory', () => { - let server: any; - let callWithInternalUser: any; - - beforeEach(() => { - callWithInternalUser = jest.fn(); - server = { - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, - }; - }); - it('should use internal user "admin"', () => { - const callWithInternalUserInstance = callWithInternalUserFactory(server); + const callWithInternalUser: any = jest.fn(); + const elasticsearchPlugin: any = { + getCluster: jest.fn(() => ({ callWithInternalUser })), + }; + const callWithInternalUserInstance = callWithInternalUserFactory(elasticsearchPlugin); callWithInternalUserInstance(); - expect(server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith('admin'); + expect(elasticsearchPlugin.getCluster).toHaveBeenCalledWith('admin'); }); }); }); diff --git a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js index 0040fcb6c802a..885573c993b7f 100644 --- a/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js +++ b/x-pack/legacy/plugins/file_upload/server/client/call_with_request_factory.js @@ -8,13 +8,13 @@ import { once } from 'lodash'; -const callWithRequest = once((server) => { - const cluster = server.plugins.elasticsearch.getCluster('data'); +const callWithRequest = once(elasticsearchPlugin => { + const cluster = elasticsearchPlugin.getCluster('data'); return cluster.callWithRequest; }); -export const callWithRequestFactory = (server, request) => { +export const callWithRequestFactory = (elasticsearchPlugin, request) => { return (...args) => { - return callWithRequest(server)(request, ...args); + return callWithRequest(elasticsearchPlugin)(request, ...args); }; }; diff --git a/x-pack/legacy/plugins/file_upload/server/plugin.js b/x-pack/legacy/plugins/file_upload/server/plugin.js new file mode 100644 index 0000000000000..0baef6f8ffa40 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/server/plugin.js @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getImportRouteHandler } from './routes/file_upload'; +import { getTelemetry, initTelemetry } from './telemetry/telemetry'; +import { MAX_BYTES } from '../common/constants/file_import'; + +const TELEMETRY_TYPE = 'fileUploadTelemetry'; + +export class FileUploadPlugin { + setup(core, plugins, __LEGACY) { + const elasticsearchPlugin = __LEGACY.plugins.elasticsearch; + const getSavedObjectsRepository = __LEGACY.savedObjects.getSavedObjectsRepository; + const makeUsageCollector = __LEGACY.usage.collectorSet.makeUsageCollector; + + // Set up route + __LEGACY.route({ + method: 'POST', + path: '/api/fileupload/import', + handler: getImportRouteHandler(elasticsearchPlugin, getSavedObjectsRepository), + config: { + payload: { maxBytes: MAX_BYTES }, + } + }); + + // Make usage collector + makeUsageCollector({ + type: TELEMETRY_TYPE, + isReady: () => true, + fetch: async () => (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry() + }); + } +} diff --git a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js index ac07d80962bdc..1eeecdeb1525b 100644 --- a/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js +++ b/x-pack/legacy/plugins/file_upload/server/routes/file_upload.js @@ -7,7 +7,6 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; import { importDataProvider } from '../models/import_data'; -import { MAX_BYTES } from '../../common/constants/file_import'; import { updateTelemetry } from '../telemetry/telemetry'; @@ -18,28 +17,35 @@ function importData({ return importDataFunc(id, index, settings, mappings, ingestPipeline, data); } -export function fileUploadRoutes(server, commonRouteConfig) { +export function getImportRouteHandler(elasticsearchPlugin, getSavedObjectsRepository) { + return async request => { - server.route({ - method: 'POST', - path: '/api/fileupload/import', - async handler(request) { + const requestObj = { + query: request.query, + payload: request.payload, + params: request.payload, + auth: request.auth, + headers: request.headers + }; - // `id` being `undefined` tells us that this is a new import due to create a new index. - // follow-up import calls to just add additional data will include the `id` of the created - // index, we'll ignore those and don't increment the counter. - const { id } = request.query; - if (id === undefined) { - await updateTelemetry({ server, ...request.payload }); - } - - const callWithRequest = callWithRequestFactory(server, request); - return importData({ callWithRequest, id, ...request.payload }) - .catch(wrapError); - }, - config: { - ...commonRouteConfig, - payload: { maxBytes: MAX_BYTES }, + // `id` being `undefined` tells us that this is a new import due to create a new index. + // follow-up import calls to just add additional data will include the `id` of the created + // index, we'll ignore those and don't increment the counter. + const { id } = requestObj.query; + if (id === undefined) { + await updateTelemetry({ elasticsearchPlugin, getSavedObjectsRepository }); } - }); + + const requestContentWithDefaults = { + id, + callWithRequest: callWithRequestFactory(elasticsearchPlugin, requestObj), + index: undefined, + settings: {}, + mappings: {}, + ingestPipeline: {}, + data: [], + ...requestObj.payload + }; + return importData(requestContentWithDefaults).catch(wrapError); + }; } diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts index d05f7cc63c896..46da040dc34f0 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts @@ -5,4 +5,3 @@ */ export * from './telemetry'; -export { makeUsageCollector } from './make_usage_collector'; diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts deleted file mode 100644 index f589280d8cf3a..0000000000000 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/make_usage_collector.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Server } from 'hapi'; -import { getTelemetry, initTelemetry, Telemetry } from './telemetry'; - -// TODO this type should be defined by the platform -interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: any; - register: any; - }; - }; -} - -export function makeUsageCollector(server: KibanaHapiServer): void { - const fileUploadUsageCollector = server.usage.collectorSet.makeUsageCollector({ - type: 'fileUploadTelemetry', - isReady: () => true, - fetch: async (): Promise => (await getTelemetry(server)) || initTelemetry(), - }); - server.usage.collectorSet.register(fileUploadUsageCollector); -} diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts index 5017c9cb41f08..1c785d8e7b61c 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.test.ts @@ -6,22 +6,13 @@ import { getTelemetry, updateTelemetry } from './telemetry'; +const elasticsearchPlugin: any = null; +const getSavedObjectsRepository: any = null; const internalRepository = () => ({ get: jest.fn(() => null), create: jest.fn(() => ({ attributes: 'test' })), update: jest.fn(() => ({ attributes: 'test' })), }); -const server: any = { - savedObjects: { - getSavedObjectsRepository: jest.fn(() => internalRepository()), - }, - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, -}; -const callWithInternalUser = jest.fn(); function mockInit(getVal: any = { attributes: {} }): any { return { @@ -34,7 +25,7 @@ describe('file upload plugin telemetry', () => { describe('getTelemetry', () => { it('should get existing telemetry', async () => { const internalRepo = mockInit(); - await getTelemetry(server, internalRepo); + await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository, internalRepo); expect(internalRepo.update.mock.calls.length).toBe(0); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); @@ -48,7 +39,12 @@ describe('file upload plugin telemetry', () => { filesUploadedTotalCount: 2, }, }); - await updateTelemetry({ server, internalRepo }); + + await updateTelemetry({ + elasticsearchPlugin, + getSavedObjectsRepository, + internalRepo, + }); expect(internalRepo.update.mock.calls.length).toBe(1); expect(internalRepo.get.mock.calls.length).toBe(1); expect(internalRepo.create.mock.calls.length).toBe(0); diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts index b43e2a1b33a29..5ffa735f4c83a 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/telemetry.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import _ from 'lodash'; import { callWithInternalUserFactory } from '../client/call_with_internal_user_factory'; @@ -18,9 +17,11 @@ export interface TelemetrySavedObject { attributes: Telemetry; } -export function getInternalRepository(server: Server): any { - const { getSavedObjectsRepository } = server.savedObjects; - const callWithInternalUser = callWithInternalUserFactory(server); +export function getInternalRepository( + elasticsearchPlugin: any, + getSavedObjectsRepository: any +): any { + const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); return getSavedObjectsRepository(callWithInternalUser); } @@ -30,8 +31,13 @@ export function initTelemetry(): Telemetry { }; } -export async function getTelemetry(server: Server, internalRepo?: object): Promise { - const internalRepository = internalRepo || getInternalRepository(server); +export async function getTelemetry( + elasticsearchPlugin: any, + getSavedObjectsRepository: any, + internalRepo?: object +): Promise { + const internalRepository = + internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); let telemetrySavedObject; try { @@ -44,14 +50,21 @@ export async function getTelemetry(server: Server, internalRepo?: object): Promi } export async function updateTelemetry({ - server, + elasticsearchPlugin, + getSavedObjectsRepository, internalRepo, }: { - server: any; + elasticsearchPlugin: any; + getSavedObjectsRepository: any; internalRepo?: any; }) { - const internalRepository = internalRepo || getInternalRepository(server); - let telemetry = await getTelemetry(server, internalRepository); + const internalRepository = + internalRepo || getInternalRepository(elasticsearchPlugin, getSavedObjectsRepository); + let telemetry = await getTelemetry( + elasticsearchPlugin, + getSavedObjectsRepository, + internalRepository + ); // Create if doesn't exist if (!telemetry || _.isEmpty(telemetry)) { const newTelemetrySavedObject = await internalRepository.create( diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js index 7c56f4c7f4316..6e931ebe2b95b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js @@ -6,7 +6,7 @@ import React from 'react'; -import { JsonUploadAndParse } from '../../../../../file_upload/public'; +import { start as fileUpload } from '../../../../../file_upload/public/legacy'; export function ClientFileCreateSourceEditor({ previewGeojsonFile, @@ -16,7 +16,7 @@ export function ClientFileCreateSourceEditor({ onIndexReady, }) { return ( - Date: Mon, 25 Nov 2019 18:28:49 +0100 Subject: [PATCH 055/128] [Console] Refactoring more legacy code and implementing minor fixes (#51312) * First part of context refactor * Finised "hook"ing in to new context for old editor output. Also fixed passing through of content type * Remove comment * - update console history behaviour - don't scroll into view on click. Doesn't really make sense. - make triple quote setting update in place --- .eslintrc.js | 6 - .../application/components/editor_example.tsx | 2 +- .../split_panel/containers/panel.tsx | 2 +- .../console_history/console_history.tsx | 58 ++++----- .../console_history/history_viewer.tsx | 8 +- .../legacy => }/console_history/index.ts | 0 .../containers/editor/context/reducer.ts | 77 ----------- .../application/containers/editor/editor.tsx | 64 +++++++++ .../application/containers/editor/index.ts | 4 +- .../legacy/console_editor/editor.test.tsx | 41 ++++-- .../editor/legacy/console_editor/editor.tsx | 45 +++---- .../legacy/console_editor/editor_output.tsx | 59 +++++++-- .../containers/editor/legacy/index.ts | 1 - .../legacy/use_ui_ace_keyboard_mode.tsx | 57 ++++----- .../application/containers/main/main.tsx | 49 ++----- .../application/containers/settings.tsx | 7 +- .../contexts/create_use_context.ts | 30 +++++ .../editor_context}/editor_context.tsx | 42 ++---- .../editor_context}/editor_registry.ts | 9 -- .../editor_context}/index.ts | 0 .../public/application/contexts/index.ts | 32 +++++ .../application/contexts/request_context.tsx | 40 ++++++ .../services_context.tsx} | 12 +- .../public/application/hooks/index.ts | 22 ++++ .../use_restore_request_from_history/index.ts | 20 +++ .../restore_request_from_history.ts | 4 + .../use_restore_request_from_history.ts | 28 ++++ .../use_send_current_request_to_es}/index.ts | 2 +- .../send_request_to_es.ts} | 121 ++++++------------ .../use_send_current_request_to_es.ts | 85 ++++++++++++ .../application/hooks/use_set_input_editor.ts | 30 +++++ .../np_ready/public/application/index.tsx | 17 +-- .../public/application/stores/editor.ts | 57 +++++++++ .../public/application/stores/request.ts | 71 ++++++++++ .../console/np_ready/public/types/common.ts | 26 ++++ 35 files changed, 737 insertions(+), 391 deletions(-) rename src/legacy/core_plugins/console/np_ready/public/application/containers/{editor/legacy => }/console_history/console_history.tsx (87%) rename src/legacy/core_plugins/console/np_ready/public/application/containers/{editor/legacy => }/console_history/history_viewer.tsx (86%) rename src/legacy/core_plugins/console/np_ready/public/application/containers/{editor/legacy => }/console_history/index.ts (100%) delete mode 100644 src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/context => contexts/editor_context}/editor_context.tsx (51%) rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/context => contexts/editor_context}/editor_registry.ts (87%) rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/context => contexts/editor_context}/index.ts (100%) create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx rename src/legacy/core_plugins/console/np_ready/public/application/{context/app_context.tsx => contexts/services_context.tsx} (77%) create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/legacy/console_history => hooks/use_restore_request_from_history}/restore_request_from_history.ts (90%) create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts rename src/legacy/core_plugins/console/np_ready/public/application/{context => hooks/use_send_current_request_to_es}/index.ts (91%) rename src/legacy/core_plugins/console/np_ready/public/application/{containers/editor/legacy/console_editor/send_current_request_to_es.ts => hooks/use_send_current_request_to_es/send_request_to_es.ts} (50%) create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts create mode 100644 src/legacy/core_plugins/console/np_ready/public/types/common.ts diff --git a/.eslintrc.js b/.eslintrc.js index 5b7dd6d6d0379..fe546ec02a668 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,12 +64,6 @@ module.exports = { 'jsx-a11y/no-onchange': 'off', }, }, - { - files: ['src/legacy/core_plugins/console/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/legacy/core_plugins/data/**/*.{js,ts,tsx}'], rules: { diff --git a/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx b/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx index 33cefd9b20968..01bd3fcd78e53 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/components/editor_example.tsx @@ -41,7 +41,7 @@ export function EditorExample(props: EditorExampleProps) { return () => { editor.destroy(); }; - }, []); + }, [elemId]); return
    ; } diff --git a/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx b/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx index 747c21433f8ed..80960a7772ba1 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/components/split_panel/containers/panel.tsx @@ -41,7 +41,7 @@ export function Panel({ children, initialWidth = '100%', style = {} }: Props) { return divRef.current!.getBoundingClientRect().width; }, }); - }, []); + }, [initialWidth, registry]); return (
    diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx similarity index 87% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx index fdfe9ecc7b94c..30966a2f77e1d 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/console_history.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/console_history.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; import moment from 'moment'; @@ -32,9 +32,10 @@ import { EuiButton, } from '@elastic/eui'; -import { useAppContext } from '../../../../context'; +import { useServicesContext } from '../../contexts'; import { HistoryViewer } from './history_viewer'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; +import { useEditorReadContext } from '../../contexts/editor_context'; +import { useRestoreRequestFromHistory } from '../../hooks'; interface Props { close: () => void; @@ -45,9 +46,8 @@ const CHILD_ELEMENT_PREFIX = 'historyReq'; export function ConsoleHistory({ close }: Props) { const { services: { history }, - } = useAppContext(); + } = useServicesContext(); - const dispatch = useEditorActionContext(); const { settings: readOnlySettings } = useEditorReadContext(); const [requests, setPastRequests] = useState(history.getHistory()); @@ -55,7 +55,7 @@ export function ConsoleHistory({ close }: Props) { const clearHistory = useCallback(() => { history.clearHistory(); setPastRequests(history.getHistory()); - }, []); + }, [history]); const listRef = useRef(null); @@ -63,14 +63,7 @@ export function ConsoleHistory({ close }: Props) { const [selectedIndex, setSelectedIndex] = useState(0); const selectedReq = useRef(null); - const scrollIntoView = (idx: number) => { - const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`); - if (activeDescendant) { - activeDescendant.scrollIntoView(); - } - }; - - const [describeReq] = useState(() => { + const describeReq = useMemo(() => { const _describeReq = (req: any) => { const endpoint = req.endpoint; const date = moment(req.time); @@ -86,34 +79,39 @@ export function ConsoleHistory({ close }: Props) { (_describeReq as any).cache = new WeakMap(); return memoize(_describeReq); - }); + }, []); + + const scrollIntoView = useCallback((idx: number) => { + const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`); + if (activeDescendant) { + activeDescendant.scrollIntoView(); + } + }, []); - const initialize = () => { + const initialize = useCallback(() => { const nextSelectedIndex = 0; (describeReq as any).cache = new WeakMap(); setViewingReq(requests[nextSelectedIndex]); selectedReq.current = requests[nextSelectedIndex]; setSelectedIndex(nextSelectedIndex); scrollIntoView(nextSelectedIndex); - }; + }, [describeReq, requests, scrollIntoView]); const clear = () => { clearHistory(); initialize(); }; - const restore = (req: any = selectedReq.current) => { - dispatch({ type: 'restoreRequest', value: req }); - }; + const restoreRequestFromHistory = useRestoreRequestFromHistory(); useEffect(() => { initialize(); - }, [requests]); + }, [initialize]); useEffect(() => { const done = history.change(setPastRequests); return () => done(); - }, []); + }, [history]); /* eslint-disable */ return ( @@ -128,7 +126,7 @@ export function ConsoleHistory({ close }: Props) { ref={listRef} onKeyDown={(ev: React.KeyboardEvent) => { if (ev.keyCode === keyCodes.ENTER) { - restore(); + restoreRequestFromHistory(selectedReq.current); return; } @@ -173,12 +171,11 @@ export function ConsoleHistory({ close }: Props) { setViewingReq(req); selectedReq.current = req; setSelectedIndex(idx); - scrollIntoView(idx); }} role="option" onMouseEnter={() => setViewingReq(req)} onMouseLeave={() => setViewingReq(selectedReq.current)} - onDoubleClick={() => restore(req)} + onDoubleClick={restoreRequestFromHistory} aria-label={i18n.translate('console.historyPage.itemOfRequestListAriaLabel', { defaultMessage: 'Request: {historyItem}', values: { historyItem: reqDescription }, @@ -196,10 +193,7 @@ export function ConsoleHistory({ close }: Props) {
    - +
    @@ -224,7 +218,11 @@ export function ConsoleHistory({ close }: Props) { - restore()}> + restoreRequestFromHistory(selectedReq.current)} + > {i18n.translate('console.historyPage.applyHistoryButtonLabel', { defaultMessage: 'Apply', })} diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx similarity index 86% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx index c15bec0563049..6fbb46bba6212 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/history_viewer.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/history_viewer.tsx @@ -21,12 +21,12 @@ import React, { useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import $ from 'jquery'; -import { DevToolsSettings } from '../../../../../services'; -import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; +import { DevToolsSettings } from '../../../services'; +import { subscribeResizeChecker } from '../editor/legacy/subscribe_console_resize_checker'; // @ts-ignore -import SenseEditor from '../../../../../../../public/quarantined/src/sense_editor/editor'; -import { applyCurrentSettings } from '../console_editor/apply_editor_settings'; +import SenseEditor from '../../../../../public/quarantined/src/sense_editor/editor'; +import { applyCurrentSettings } from '../editor/legacy/console_editor/apply_editor_settings'; interface Props { settings: DevToolsSettings; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/index.ts similarity index 100% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/index.ts rename to src/legacy/core_plugins/console/np_ready/public/application/containers/console_history/index.ts diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts deleted file mode 100644 index caed6b24c3c11..0000000000000 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/reducer.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Reducer } from 'react'; - -import { instance as registry } from './editor_registry'; -import { ContextValue } from './editor_context'; - -import { restoreRequestFromHistory } from '../legacy/console_history/restore_request_from_history'; -import { - sendCurrentRequestToES, - EsRequestArgs, -} from '../legacy/console_editor/send_current_request_to_es'; -import { DevToolsSettings } from '../../../../services'; - -export type Action = - | { type: 'setInputEditor'; value: any } - | { type: 'setOutputEditor'; value: any } - | { type: 'restoreRequest'; value: any } - | { type: 'updateSettings'; value: DevToolsSettings } - | { type: 'sendRequestToEs'; value: EsRequestArgs } - | { type: 'updateRequestHistory'; value: any }; - -export const reducer: Reducer = (state, action) => { - const nextState = { ...state }; - - if (action.type === 'setInputEditor') { - registry.setInputEditor(action.value); - if (registry.getOutputEditor()) { - nextState.editorsReady = true; - } - } - - if (action.type === 'setOutputEditor') { - registry.setOutputEditor(action.value); - if (registry.getInputEditor()) { - nextState.editorsReady = true; - } - } - - if (action.type === 'restoreRequest') { - restoreRequestFromHistory(registry.getInputEditor(), action.value); - } - - if (action.type === 'updateSettings') { - nextState.settings = action.value; - } - - if (action.type === 'sendRequestToEs') { - const { callback, isPolling, isUsingTripleQuotes } = action.value; - sendCurrentRequestToES({ - input: registry.getInputEditor(), - output: registry.getOutputEditor(), - callback, - isUsingTripleQuotes, - isPolling, - }); - } - - return nextState; -}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx new file mode 100644 index 0000000000000..07b48c083bf61 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/editor.tsx @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback } from 'react'; +import { debounce } from 'lodash'; + +import { Panel, PanelsContainer } from '../../components/split_panel'; +import { Editor as EditorUI, EditorOutput } from './legacy/console_editor'; +import { StorageKeys } from '../../../services'; +import { useServicesContext } from '../../contexts'; + +const INITIAL_PANEL_WIDTH = 50; +const PANEL_MIN_WIDTH = '100px'; + +export const Editor = () => { + const { + services: { storage }, + } = useServicesContext(); + + const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ + INITIAL_PANEL_WIDTH, + INITIAL_PANEL_WIDTH, + ]); + + const onPanelWidthChange = useCallback( + debounce((widths: number[]) => { + storage.set(StorageKeys.WIDTH, widths); + }, 300), + [] + ); + + return ( + + + + + + + + + ); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts index b3cab3d13b3a3..87436d7f97389 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { Editor, EditorOutput, ConsoleHistory, autoIndent, getDocumentation } from './legacy'; -export { useEditorActionContext, useEditorReadContext, EditorContextProvider } from './context'; +export { autoIndent, getDocumentation } from './legacy'; +export { Editor } from './editor'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx index 03d5b3f1d8f44..cb5559edfb249 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -20,15 +20,29 @@ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; +import { act } from 'react-dom/test-utils'; import * as sinon from 'sinon'; -import { EditorContextProvider } from '../../context'; -import { AppContextProvider } from '../../../../context'; +import { + ServicesContextProvider, + EditorContextProvider, + RequestContextProvider, +} from '../../../../contexts'; + import { Editor } from './editor'; +jest.mock('../../../../contexts/editor_context/editor_registry.ts', () => ({ + instance: { + setInputEditor: () => {}, + getInputEditor: () => ({ + getRequestsInRange: (cb: any) => cb([{ test: 'test' }]), + }), + }, +})); jest.mock('../../../../components/editor_example.tsx', () => {}); jest.mock('../../../../../../../public/quarantined/src/mappings.js', () => ({ retrieveAutoCompleteInfo: () => {}, + clearSubscriptions: () => {}, })); jest.mock('../../../../../../../public/quarantined/src/input.ts', () => { return { @@ -46,7 +60,7 @@ jest.mock('../../../../../../../public/quarantined/src/input.ts', () => { }; }); -import * as sendRequestModule from './send_current_request_to_es'; +import * as sendRequestModule from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; import * as consoleMenuActions from '../console_menu_actions'; describe('Legacy (Ace) Console Editor Component Smoke Test', () => { @@ -66,19 +80,24 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { }; editor = mount( - - - - - + + + + + + + ); }); - it('calls send current request to ES', () => { - const stub = sinon.stub(sendRequestModule, 'sendCurrentRequestToES'); + // TODO: Re-enable when React ^16.9 is available + it.skip('calls send current request to ES', () => { + const stub = sinon.stub(sendRequestModule, 'sendRequestToES'); try { - editor.find('[data-test-subj~="sendRequestButton"]').simulate('click'); + act(() => { + editor.find('[data-test-subj~="sendRequestButton"]').simulate('click'); + }); expect(stub.called).toBe(true); expect(stub.callCount).toBe(1); } finally { diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx index 10f1ef34602a6..0fa0ec732c770 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import $ from 'jquery'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useAppContext } from '../../../../context'; +import { useServicesContext, useEditorReadContext } from '../../../../contexts'; import { useUIAceKeyboardMode } from '../use_ui_ace_keyboard_mode'; import { ConsoleMenu } from '../../../../components'; @@ -32,12 +32,13 @@ import { autoIndent, getDocumentation } from '../console_menu_actions'; import { registerCommands } from './keyboard_shortcuts'; import { applyCurrentSettings } from './apply_editor_settings'; +import { useSendCurrentRequestToES, useSetInputEditor } from '../../../../hooks'; + // @ts-ignore import { initializeEditor } from '../../../../../../../public/quarantined/src/input'; // @ts-ignore import mappings from '../../../../../../../public/quarantined/src/mappings'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; import { loadRemoteState } from './load_remote_editor_state'; @@ -60,14 +61,15 @@ const DEFAULT_INPUT_VALUE = `GET _search } }`; -function _Editor({ previousStateLocation = 'stored' }: EditorProps) { +function EditorUI({ previousStateLocation = 'stored' }: EditorProps) { const { services: { history, notifications }, docLinkVersion, - } = useAppContext(); + } = useServicesContext(); const { settings } = useEditorReadContext(); - const dispatch = useEditorActionContext(); + const setInputEditor = useSetInputEditor(); + const sendCurrentRequestToES = useSendCurrentRequestToES(); const editorRef = useRef(null); const actionsRef = useRef(null); @@ -76,13 +78,13 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { const [textArea, setTextArea] = useState(null); useUIAceKeyboardMode(textArea); - const openDocumentation = async () => { + const openDocumentation = useCallback(async () => { const documentation = await getDocumentation(editorInstanceRef.current!, docLinkVersion); if (!documentation) { return; } window.open(documentation, '_blank'); - }; + }, [docLinkVersion]); useEffect(() => { const $editor = $(editorRef.current!); @@ -102,7 +104,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { let timer: number; const saveDelay = 500; - return editorInstanceRef.current.getSession().on('change', function onChange() { + editorInstanceRef.current.getSession().on('change', function onChange() { if (timer) { clearTimeout(timer); } @@ -119,11 +121,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { } } - dispatch({ - type: 'setInputEditor', - value: editorInstanceRef.current, - }); - + setInputEditor(editorInstanceRef.current); setTextArea(editorRef.current!.querySelector('textarea')); mappings.retrieveAutoCompleteInfo(); @@ -132,26 +130,13 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { editorRef.current!, editorInstanceRef.current ); - const unsubscribeAutoSave = setupAutosave(); + setupAutosave(); return () => { unsubscribeResizer(); - unsubscribeAutoSave(); mappings.clearSubscriptions(); }; - }, []); - - const sendCurrentRequestToES = useCallback(() => { - dispatch({ - type: 'sendRequestToEs', - value: { - isUsingTripleQuotes: settings.tripleQuotes, - isPolling: settings.polling, - callback: (esPath: any, esMethod: any, esData: any) => - history.addToHistory(esPath, esMethod, esData), - }, - }); - }, [settings]); + }, [history, previousStateLocation, setInputEditor]); useEffect(() => { applyCurrentSettings(editorInstanceRef.current!, settings); @@ -165,7 +150,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { sendCurrentRequestToES, openDocumentation, }); - }, [sendCurrentRequestToES]); + }, [sendCurrentRequestToES, openDocumentation]); return (
    @@ -219,4 +204,4 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { ); } -export const Editor = React.memo(_Editor); +export const Editor = React.memo(EditorUI); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx index d38e86df41464..c167155bd18a9 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -16,39 +16,70 @@ * specific language governing permissions and limitations * under the License. */ + import React, { useEffect, useRef } from 'react'; import $ from 'jquery'; // @ts-ignore import { initializeOutput } from '../../../../../../../public/quarantined/src/output'; -import { useAppContext } from '../../../../context'; -import { useEditorActionContext, useEditorReadContext } from '../../context'; +import { + useServicesContext, + useEditorReadContext, + useRequestReadContext, +} from '../../../../contexts'; + +// @ts-ignore +import utils from '../../../../../../../public/quarantined/src/utils'; + import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; import { applyCurrentSettings } from './apply_editor_settings'; -function _EditorOuput() { +function modeForContentType(contentType: string) { + if (contentType.indexOf('application/json') >= 0) { + return 'ace/mode/json'; + } else if (contentType.indexOf('application/yaml') >= 0) { + return 'ace/mode/yaml'; + } + return 'ace/mode/text'; +} + +function EditorOutputUI() { const editorRef = useRef(null); const editorInstanceRef = useRef(null); - const { - services: { settings }, - } = useAppContext(); - - const dispatch = useEditorActionContext(); + const { services } = useServicesContext(); const { settings: readOnlySettings } = useEditorReadContext(); + const { + lastResult: { data, error }, + } = useRequestReadContext(); useEffect(() => { const editor$ = $(editorRef.current!); - editorInstanceRef.current = initializeOutput(editor$, settings); - editorInstanceRef.current.update(''); + editorInstanceRef.current = initializeOutput(editor$, services.settings); const unsubscribe = subscribeResizeChecker(editorRef.current!, editorInstanceRef.current); - dispatch({ type: 'setOutputEditor', value: editorInstanceRef.current }); - return () => { unsubscribe(); }; - }, []); + }, [services.settings]); + + useEffect(() => { + if (data) { + const mode = modeForContentType(data[0].response.contentType); + editorInstanceRef.current.session.setMode(mode); + editorInstanceRef.current.update( + data + .map(d => d.response.value) + .map(readOnlySettings.tripleQuotes ? utils.expandLiteralStrings : a => a) + .join('\n') + ); + } else if (error) { + editorInstanceRef.current.session.setMode(modeForContentType(error.contentType)); + editorInstanceRef.current.update(error.value); + } else { + editorInstanceRef.current.update(''); + } + }, [readOnlySettings, data, error]); useEffect(() => { applyCurrentSettings(editorInstanceRef.current, readOnlySettings); @@ -61,4 +92,4 @@ function _EditorOuput() { ); } -export const EditorOutput = React.memo(_EditorOuput); +export const EditorOutput = React.memo(EditorOutputUI); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts index 134f3de42833b..832295d4eb00b 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/index.ts @@ -18,5 +18,4 @@ */ export { EditorOutput, Editor } from './console_editor'; -export { ConsoleHistory } from './console_history'; export { getDocumentation, autoIndent } from './console_menu_actions'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx index 269f4e2cdeb72..ca74b19b76f16 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/use_ui_ace_keyboard_mode.tsx @@ -35,41 +35,40 @@ export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | n const overlayMountNode = useRef(null); const autoCompleteVisibleRef = useRef(false); - function onDismissOverlay(event: KeyboardEvent) { - if (event.keyCode === keyCodes.ENTER) { - event.preventDefault(); - aceTextAreaElement!.focus(); - } - } - - function enableOverlay() { - if (overlayMountNode.current) { - overlayMountNode.current.focus(); + useEffect(() => { + function onDismissOverlay(event: KeyboardEvent) { + if (event.keyCode === keyCodes.ENTER) { + event.preventDefault(); + aceTextAreaElement!.focus(); + } } - } - const isAutoCompleteVisible = () => { - const autoCompleter = document.querySelector('.ace_autocomplete'); - if (!autoCompleter) { - return false; + function enableOverlay() { + if (overlayMountNode.current) { + overlayMountNode.current.focus(); + } } - // The autoComplete is just hidden when it's closed, not removed from the DOM. - return autoCompleter.style.display !== 'none'; - }; - const documentKeyDownListener = () => { - autoCompleteVisibleRef.current = isAutoCompleteVisible(); - }; + const isAutoCompleteVisible = () => { + const autoCompleter = document.querySelector('.ace_autocomplete'); + if (!autoCompleter) { + return false; + } + // The autoComplete is just hidden when it's closed, not removed from the DOM. + return autoCompleter.style.display !== 'none'; + }; - const aceKeydownListener = (event: KeyboardEvent) => { - if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) { - event.preventDefault(); - event.stopPropagation(); - enableOverlay(); - } - }; + const documentKeyDownListener = () => { + autoCompleteVisibleRef.current = isAutoCompleteVisible(); + }; - useEffect(() => { + const aceKeydownListener = (event: KeyboardEvent) => { + if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) { + event.preventDefault(); + event.stopPropagation(); + enableOverlay(); + } + }; if (aceTextAreaElement) { // We don't control HTML elements inside of ace so we imperatively create an element // that acts as a container and insert it just before ace's textarea element diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx index 518630c5a07c1..764c4b8e87100 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx @@ -17,33 +17,25 @@ * under the License. */ -import React, { useCallback, useState } from 'react'; -import { debounce } from 'lodash'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; - -import { EditorOutput, Editor, ConsoleHistory } from '../editor'; +import { ConsoleHistory } from '../console_history'; +import { Editor } from '../editor'; import { Settings } from '../settings'; -// TODO: find out what this is: $(document.body).removeClass('fouc'); - -import { TopNavMenu, WelcomePanel, HelpPanel, PanelsContainer, Panel } from '../../components'; +import { TopNavMenu, WelcomePanel, HelpPanel } from '../../components'; -import { useAppContext } from '../../context'; -import { StorageKeys } from '../../../services'; +import { useServicesContext, useEditorReadContext } from '../../contexts'; import { getTopNavConfig } from './get_top_nav'; -import { useEditorReadContext } from '../editor'; - -const INITIAL_PANEL_WIDTH = 50; -const PANEL_MIN_WIDTH = '100px'; export function Main() { const { services: { storage }, - } = useAppContext(); + } = useServicesContext(); - const { editorsReady } = useEditorReadContext(); + const { ready: editorsReady } = useEditorReadContext(); const [showWelcome, setShowWelcomePanel] = useState( () => storage.get('version_welcome_shown') !== '@@SENSE_REVISION' @@ -53,18 +45,6 @@ export function Main() { const [showSettings, setShowSettings] = useState(false); const [showHelp, setShowHelp] = useState(false); - const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ - INITIAL_PANEL_WIDTH, - INITIAL_PANEL_WIDTH, - ]); - - const onPanelWidthChange = useCallback( - debounce((widths: number[]) => { - storage.set(StorageKeys.WIDTH, widths); - }, 300), - [] - ); - const renderConsoleHistory = () => { return editorsReady ? setShowHistory(false)} /> : null; }; @@ -95,20 +75,7 @@ export function Main() { {showingHistory ? {renderConsoleHistory()} : null} - - - - - - - - + diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx index d794dc9302c25..8440faa6eeea8 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/settings.tsx @@ -22,9 +22,8 @@ import { AutocompleteOptions, DevToolsSettingsModal } from '../components'; // @ts-ignore import mappings from '../../../../public/quarantined/src/mappings'; -import { useAppContext } from '../context'; +import { useServicesContext, useEditorActionContext } from '../contexts'; import { DevToolsSettings } from '../../services'; -import { useEditorActionContext } from './editor/context'; const getAutocompleteDiff = (newSettings: DevToolsSettings, prevSettings: DevToolsSettings) => { return Object.keys(newSettings.autocomplete).filter(key => { @@ -76,7 +75,7 @@ export interface Props { export function Settings({ onClose }: Props) { const { services: { settings }, - } = useAppContext(); + } = useServicesContext(); const dispatch = useEditorActionContext(); @@ -90,7 +89,7 @@ export function Settings({ onClose }: Props) { // Let the rest of the application know settings have updated. dispatch({ type: 'updateSettings', - value: newSettings, + payload: newSettings, }); onClose(); }; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts new file mode 100644 index 0000000000000..03b93c7d5b8ba --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/create_use_context.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Context, useContext } from 'react'; + +export const createUseContext = (Ctx: Context, name: string) => { + return () => { + const ctx = useContext(Ctx); + if (!ctx) { + throw new Error(`${name} should be used inside of ${name}Provider!`); + } + return ctx; + }; +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx similarity index 51% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx index aa04a5ff3dd96..d5ed44e3f6ba2 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_context.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_context.tsx @@ -17,52 +17,30 @@ * under the License. */ -import React, { createContext, Dispatch, useContext, useReducer } from 'react'; -import { Action, reducer } from './reducer'; -import { DevToolsSettings } from '../../../../services'; +import React, { createContext, Dispatch, useReducer } from 'react'; +import * as editor from '../../stores/editor'; +import { DevToolsSettings } from '../../../services'; +import { createUseContext } from '../create_use_context'; -export interface ContextValue { - editorsReady: boolean; - settings: DevToolsSettings; -} - -const EditorReadContext = createContext(null as any); -const EditorActionContext = createContext>(null as any); +const EditorReadContext = createContext(null as any); +const EditorActionContext = createContext>(null as any); export interface EditorContextArgs { children: any; settings: DevToolsSettings; } -const initialValue: ContextValue = { - editorsReady: false, - settings: null as any, -}; - export function EditorContextProvider({ children, settings }: EditorContextArgs) { - const [state, dispatch] = useReducer(reducer, initialValue, value => ({ + const [state, dispatch] = useReducer(editor.reducer, editor.initialValue, value => ({ ...value, settings, })); return ( - + {children} ); } -export const useEditorActionContext = () => { - const context = useContext(EditorActionContext); - if (context === undefined) { - throw new Error('useEditorActionContext must be used inside EditorActionContext'); - } - return context; -}; - -export const useEditorReadContext = () => { - const context = useContext(EditorReadContext); - if (context === undefined) { - throw new Error('useEditorReadContext must be used inside EditorContextProvider'); - } - return context; -}; +export const useEditorReadContext = createUseContext(EditorReadContext, 'EditorReadContext'); +export const useEditorActionContext = createUseContext(EditorActionContext, 'EditorActionContext'); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts similarity index 87% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts index 6f14c6fc84150..bdccc1af0860c 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/editor_registry.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/editor_registry.ts @@ -19,23 +19,14 @@ export class EditorRegistry { inputEditor: any; - outputEditor: any; setInputEditor(inputEditor: any) { this.inputEditor = inputEditor; } - setOutputEditor(outputEditor: any) { - this.outputEditor = outputEditor; - } - getInputEditor() { return this.inputEditor; } - - getOutputEditor() { - return this.outputEditor; - } } // Create a single instance of this and use as private state. diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/index.ts similarity index 100% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/context/index.ts rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/editor_context/index.ts diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts new file mode 100644 index 0000000000000..18234acf15957 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/index.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useServicesContext, ServicesContextProvider } from './services_context'; + +export { + useRequestActionContext, + useRequestReadContext, + RequestContextProvider, +} from './request_context'; + +export { + useEditorActionContext, + useEditorReadContext, + EditorContextProvider, +} from './editor_context'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx new file mode 100644 index 0000000000000..faaa3196a97bc --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/request_context.tsx @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { createContext, useReducer, Dispatch } from 'react'; +import { createUseContext } from './create_use_context'; +import * as store from '../stores/request'; + +const RequestReadContext = createContext(null as any); +const RequestActionContext = createContext>(null as any); + +export function RequestContextProvider({ children }: { children: React.ReactNode }) { + const [state, dispatch] = useReducer(store.reducer, store.initialValue); + return ( + + {children} + + ); +} + +export const useRequestReadContext = createUseContext(RequestReadContext, 'RequestReadContext'); +export const useRequestActionContext = createUseContext( + RequestActionContext, + 'RequestActionContext' +); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx b/src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx similarity index 77% rename from src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx rename to src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx index be7aa87ac2894..f715b1ae53a78 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/context/app_context.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/contexts/services_context.tsx @@ -18,7 +18,7 @@ */ import React, { createContext, useContext } from 'react'; -import { NotificationsSetup } from '../../../../../../../core/public'; +import { NotificationsSetup } from 'kibana/public'; import { History, Storage, Settings } from '../../services'; interface ContextValue { @@ -36,14 +36,14 @@ interface ContextProps { children: any; } -const AppContext = createContext(null as any); +const ServicesContext = createContext(null as any); -export function AppContextProvider({ children, value }: ContextProps) { - return {children}; +export function ServicesContextProvider({ children, value }: ContextProps) { + return {children}; } -export const useAppContext = () => { - const context = useContext(AppContext); +export const useServicesContext = () => { + const context = useContext(ServicesContext); if (context === undefined) { throw new Error('useAppContext must be used inside the AppContextProvider.'); } diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts new file mode 100644 index 0000000000000..8c5a8d599a0df --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useSetInputEditor } from './use_set_input_editor'; +export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; +export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts new file mode 100644 index 0000000000000..017344ae537ab --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts similarity index 90% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts rename to src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts index f7f691e083ca2..b053e605b5fae 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_history/restore_request_from_history.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/restore_request_from_history.ts @@ -17,6 +17,10 @@ * under the License. */ +/** + * This function is considered legacy and should not be changed or updated before we have editor + * interfaces in place (it's using a customized version of Ace directly). + */ export function restoreRequestFromHistory(input: any, req: any) { const session = input.getSession(); let pos = input.getCursorPosition(); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts new file mode 100644 index 0000000000000..590ad78e6c236 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_restore_request_from_history/use_restore_request_from_history.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useCallback } from 'react'; +import { instance as registry } from '../../contexts/editor_context/editor_registry'; +import { restoreRequestFromHistory } from './restore_request_from_history'; + +export const useRestoreRequestFromHistory = () => { + return useCallback((req: any) => { + const editor = registry.getInputEditor(); + restoreRequestFromHistory(editor, req); + }, []); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/context/index.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts similarity index 91% rename from src/legacy/core_plugins/console/np_ready/public/application/context/index.ts rename to src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts index 27d69f5736ffe..a8f59d573c1a0 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/context/index.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { useAppContext, AppContextProvider } from './app_context'; +export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts similarity index 50% rename from src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts rename to src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index d3abf9c92f48e..22fa4477e9012 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/send_current_request_to_es.ts +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -18,55 +18,49 @@ */ // @ts-ignore -import mappings from '../../../../../../../public/quarantined/src/mappings'; +import utils from '../../../../../public/quarantined/src/utils'; // @ts-ignore -import utils from '../../../../../../../public/quarantined/src/utils'; -// @ts-ignore -import * as es from '../../../../../../../public/quarantined/src/es'; +import * as es from '../../../../../public/quarantined/src/es'; +import { BaseResponseType } from '../../../types/common'; export interface EsRequestArgs { - callback: (esPath: any, esMethod: any, esData: any) => void; - input?: any; - output?: any; - isPolling: boolean; - isUsingTripleQuotes: boolean; + requests: any; +} + +export interface ESRequestResult { + request: { + path: string; + data: any; + method: string; + }; + response: { + contentType: BaseResponseType; + value: unknown; + }; } let CURRENT_REQ_ID = 0; -export function sendCurrentRequestToES({ - callback, - input, - output, - isPolling, - isUsingTripleQuotes, -}: EsRequestArgs) { - const reqId = ++CURRENT_REQ_ID; - - input.getRequestsInRange((requests: any) => { +export function sendRequestToES({ requests }: EsRequestArgs): Promise { + return new Promise((resolve, reject) => { + const reqId = ++CURRENT_REQ_ID; + const results: ESRequestResult[] = []; if (reqId !== CURRENT_REQ_ID) { return; } - if (output) { - output.update(''); - } if (requests.length === 0) { return; } const isMultiRequest = requests.length > 1; - const finishChain = () => { - /* noop */ - }; - - let isFirstRequest = true; const sendNextRequest = () => { if (reqId !== CURRENT_REQ_ID) { + resolve(results); return; } if (requests.length === 0) { - finishChain(); + resolve(results); return; } const req = requests.shift(); @@ -85,41 +79,13 @@ export function sendCurrentRequestToES({ const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown; - function modeForContentType(contentType: string) { - if (contentType.indexOf('text/plain') >= 0) { - return 'ace/mode/text'; - } else if (contentType.indexOf('application/yaml') >= 0) { - return 'ace/mode/yaml'; - } - return null; - } - const isSuccess = typeof xhr.status === 'number' && // Things like DELETE index where the index is not there are OK. ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404); if (isSuccess) { - if (xhr.status !== 404 && isPolling) { - // If the user has submitted a request against ES, something in the fields, indices, aliases, - // or templates may have changed, so we'll need to update this data. Assume that if - // the user disables polling they're trying to optimize performance or otherwise - // preserve resources, so they won't want this request sent either. - mappings.retrieveAutoCompleteInfo(); - } - let value = xhr.responseText; - const mode = modeForContentType(xhr.getAllResponseHeaders('Content-Type') || ''); - - // Apply triple quotes to output. - if (isUsingTripleQuotes && mode === null) { - // assume json - auto pretty - try { - value = utils.expandLiteralStrings(value); - } catch (e) { - // nothing to do here - } - } const warnings = xhr.getResponseHeader('warning'); if (warnings) { @@ -131,47 +97,34 @@ export function sendCurrentRequestToES({ value = '# ' + req.method + ' ' + req.url + '\n' + value; } - if (output) { - if (isFirstRequest) { - output.update(value, mode); - } else { - output.append('\n' + value); - } - } + results.push({ + response: { + contentType: xhr.getResponseHeader('Content-Type'), + value, + }, + request: { + data: esData, + method: esMethod, + path: esPath, + }, + }); - isFirstRequest = false; // single request terminate via sendNextRequest as well - - callback(esPath, esMethod, esData); sendNextRequest(); } else { let value; - let mode; + let contentType: string; if (xhr.responseText) { value = xhr.responseText; // ES error should be shown - mode = modeForContentType(xhr.getAllResponseHeaders('Content-Type') || ''); - if (value[0] === '{') { - try { - value = JSON.stringify(JSON.parse(value), null, 2); - } catch (e) { - // nothing to do here - } - } + contentType = xhr.getResponseHeader('Content-Type'); } else { value = 'Request failed to get to the server (status code: ' + xhr.status + ')'; - mode = 'ace/mode/text'; + contentType = 'text/plain'; } if (isMultiRequest) { value = '# ' + req.method + ' ' + req.url + '\n' + value; } - if (output) { - if (isFirstRequest) { - output.update(value, mode); - } else { - output.append('\n' + value); - } - } - finishChain(); + reject({ value, contentType }); } } ); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts new file mode 100644 index 0000000000000..63d1120808e02 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { i18n } from '@kbn/i18n'; +import { useCallback } from 'react'; +import { instance as registry } from '../../contexts/editor_context/editor_registry'; +import { useServicesContext } from '../../contexts'; +import { sendRequestToES } from './send_request_to_es'; +import { useRequestActionContext } from '../../contexts'; +// @ts-ignore +import mappings from '../../../../../public/quarantined/src/mappings'; + +export const useSendCurrentRequestToES = () => { + const { + services: { history, settings, notifications }, + } = useServicesContext(); + + const dispatch = useRequestActionContext(); + + return useCallback(async () => { + dispatch({ type: 'sendRequest', payload: undefined }); + try { + const editor = registry.getInputEditor(); + const requests = await new Promise(resolve => editor.getRequestsInRange(resolve)); + if (!requests.length) { + dispatch({ + type: 'requestFail', + payload: { value: 'No requests in range', contentType: 'text/plain' }, + }); + return; + } + const results = await sendRequestToES({ + requests, + }); + + results.forEach(({ request: { path, method, data } }) => { + history.addToHistory(path, method, data); + }); + + const { polling } = settings.toJSON(); + if (polling) { + // If the user has submitted a request against ES, something in the fields, indices, aliases, + // or templates may have changed, so we'll need to update this data. Assume that if + // the user disables polling they're trying to optimize performance or otherwise + // preserve resources, so they won't want this request sent either. + mappings.retrieveAutoCompleteInfo(); + } + + dispatch({ + type: 'requestSuccess', + payload: { + data: results, + }, + }); + } catch (e) { + if (e.contentType) { + dispatch({ + type: 'requestFail', + payload: e, + }); + } else { + notifications.toasts.addError(e, { + title: i18n.translate('console.unknownRequestErrorTitle', { + defaultMessage: 'Unknown Request Error', + }), + }); + } + } + }, [dispatch, settings, history, notifications]); +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts new file mode 100644 index 0000000000000..672f3e269ead9 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/hooks/use_set_input_editor.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useEditorActionContext } from '../contexts/editor_context'; +import { instance as registry } from '../contexts/editor_context/editor_registry'; + +export const useSetInputEditor = () => { + const dispatch = useEditorActionContext(); + + return (editor: any) => { + dispatch({ type: 'setInputEditor', payload: editor }); + registry.setInputEditor(editor); + }; +}; diff --git a/src/legacy/core_plugins/console/np_ready/public/application/index.tsx b/src/legacy/core_plugins/console/np_ready/public/application/index.tsx index aaacfd3894d18..e181caf23d2cb 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/index.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/index.tsx @@ -18,9 +18,8 @@ */ import React from 'react'; -import { NotificationsSetup } from '../../../../../../core/public'; -import { AppContextProvider } from './context'; -import { EditorContextProvider } from './containers/editor/context'; +import { NotificationsSetup } from 'src/core/public'; +import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts'; import { Main } from './containers'; import { createStorage, createHistory, createSettings, Settings } from '../services'; @@ -46,16 +45,18 @@ export function boot(deps: { return ( - - -
    - - + + +
    + + + ); } diff --git a/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts b/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts new file mode 100644 index 0000000000000..339a2f7a2c4af --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/stores/editor.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Reducer } from 'react'; +import { produce } from 'immer'; +import { identity } from 'fp-ts/lib/function'; +import { DevToolsSettings } from '../../services'; + +export interface Store { + ready: boolean; + settings: DevToolsSettings; +} + +export const initialValue: Store = produce( + { + ready: false, + settings: null as any, + }, + identity +); + +export type Action = + | { type: 'setInputEditor'; payload: any } + | { type: 'updateSettings'; payload: DevToolsSettings }; + +export const reducer: Reducer = (state, action) => + produce(state, draft => { + if (action.type === 'setInputEditor') { + if (action.payload) { + draft.ready = true; + } + return; + } + + if (action.type === 'updateSettings') { + draft.settings = action.payload; + return; + } + + return draft; + }); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts b/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts new file mode 100644 index 0000000000000..fec7f4195eb74 --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/application/stores/request.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Reducer } from 'react'; +import { produce } from 'immer'; +import { identity } from 'fp-ts/lib/function'; +import { BaseResponseType } from '../../types/common'; +import { ESRequestResult } from '../hooks/use_send_current_request_to_es/send_request_to_es'; + +export type Actions = + | { type: 'sendRequest'; payload: undefined } + | { type: 'requestSuccess'; payload: { data: ESRequestResult[] } } + | { type: 'requestFail'; payload: { contentType: BaseResponseType; value: string } }; + +export interface Store { + requestInFlight: boolean; + lastResult: { + data: ESRequestResult[] | null; + error?: { contentType: BaseResponseType; value: string }; + }; +} + +const initialResultValue = { + data: null, + type: 'unknown' as BaseResponseType, +}; + +export const initialValue: Store = produce( + { + requestInFlight: false, + lastResult: initialResultValue, + }, + identity +); + +export const reducer: Reducer = (state, action) => + produce(state, draft => { + if (action.type === 'sendRequest') { + draft.requestInFlight = true; + draft.lastResult = initialResultValue; + return; + } + + if (action.type === 'requestSuccess') { + draft.requestInFlight = false; + draft.lastResult = action.payload; + return; + } + + if (action.type === 'requestFail') { + draft.requestInFlight = false; + draft.lastResult = { ...initialResultValue, error: action.payload }; + return; + } + }); diff --git a/src/legacy/core_plugins/console/np_ready/public/types/common.ts b/src/legacy/core_plugins/console/np_ready/public/types/common.ts new file mode 100644 index 0000000000000..ad9ed10d4188f --- /dev/null +++ b/src/legacy/core_plugins/console/np_ready/public/types/common.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type BaseResponseType = + | 'application/json' + | 'text/csv' + | 'text/tab-separated-values' + | 'text/plain' + | 'application/yaml' + | 'unknown'; From 03dad2827ecad7ec2d5dd6591a3f451b092c4e5e Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 25 Nov 2019 11:39:20 -0700 Subject: [PATCH 056/128] run mocha tests from x-pack with root mocha script (#51352) * run mocha tests from x-pack with root mocha script * Only run Karma tests in xpack intake job * disable failing suites * fix typo * skip correct suite (there are multiple root suites) * support disabling junit reporting with $DISABLE_JUNIT_REPORTER * don't generate junit in ispec_plugin tests --- .../integration_tests/generate_plugin.test.js | 14 +++- .../lib/config/schema.ts | 2 +- .../kbn-test/src/mocha/auto_junit_reporter.js | 2 +- packages/kbn-test/src/mocha/run_mocha_cli.js | 11 ++- src/dev/jest/junit_reporter.js | 2 +- tasks/config/karma.js | 20 ++++-- test/scripts/jenkins_xpack.sh | 2 +- x-pack/gulpfile.js | 3 +- .../__tests__/grokdebugger_request.js | 3 +- .../__tests__/get_all_stats.js | 3 +- .../__tests__/get_cluster_uuids.js | 3 +- .../__tests__/helpers/cancellation_token.js | 3 +- .../server/lib/esqueue/__tests__/worker.js | 4 +- .../lib/validate/__tests__/validate_config.js | 3 +- x-pack/package.json | 4 +- x-pack/tasks/test.ts | 27 +------- yarn.lock | 68 ------------------- 17 files changed, 59 insertions(+), 115 deletions(-) diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index b43dcd80b4462..aa6611f3b6738 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -70,11 +70,21 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug describe(`then running`, () => { it(`'yarn test:browser' should exit 0`, async () => { - await execa('yarn', ['test:browser'], { cwd: generatedPath }); + await execa('yarn', ['test:browser'], { + cwd: generatedPath, + env: { + DISABLE_JUNIT_REPORTER: '1', + }, + }); }); it(`'yarn test:server' should exit 0`, async () => { - await execa('yarn', ['test:server'], { cwd: generatedPath }); + await execa('yarn', ['test:server'], { + cwd: generatedPath, + env: { + DISABLE_JUNIT_REPORTER: '1', + }, + }); }); it(`'yarn build' should exit 0`, async () => { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 52672d5f039fb..4530b61423620 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -143,7 +143,7 @@ export const schema = Joi.object() junit: Joi.object() .keys({ - enabled: Joi.boolean().default(!!process.env.CI), + enabled: Joi.boolean().default(!!process.env.CI && !process.env.DISABLE_JUNIT_REPORTER), reportName: Joi.string(), }) .default(), diff --git a/packages/kbn-test/src/mocha/auto_junit_reporter.js b/packages/kbn-test/src/mocha/auto_junit_reporter.js index 50b589fbc57a5..b6e79616e1cde 100644 --- a/packages/kbn-test/src/mocha/auto_junit_reporter.js +++ b/packages/kbn-test/src/mocha/auto_junit_reporter.js @@ -29,7 +29,7 @@ export function createAutoJUnitReporter(junitReportOptions) { new MochaSpecReporter(runner, options); // in CI we also setup the JUnit reporter - if (process.env.CI) { + if (process.env.CI && !process.env.DISABLE_JUNIT_REPORTER) { setupJUnitReportGeneration(runner, junitReportOptions); } } diff --git a/packages/kbn-test/src/mocha/run_mocha_cli.js b/packages/kbn-test/src/mocha/run_mocha_cli.js index 7a90108472721..77f40aded1d7f 100644 --- a/packages/kbn-test/src/mocha/run_mocha_cli.js +++ b/packages/kbn-test/src/mocha/run_mocha_cli.js @@ -63,7 +63,16 @@ export function runMochaCli() { if (!opts._.length) { globby .sync( - ['src/**/__tests__/**/*.js', 'packages/**/__tests__/**/*.js', 'tasks/**/__tests__/**/*.js'], + [ + 'src/**/__tests__/**/*.js', + 'packages/**/__tests__/**/*.js', + 'tasks/**/__tests__/**/*.js', + 'x-pack/common/**/__tests__/**/*.js', + 'x-pack/server/**/__tests__/**/*.js', + `x-pack/legacy/plugins/*/__tests__/**/*.js`, + `x-pack/legacy/plugins/*/common/**/__tests__/**/*.js`, + `x-pack/legacy/plugins/*/**/server/**/__tests__/**/*.js`, + ], { cwd: REPO_ROOT, onlyFiles: true, diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 30501965bf1e7..7f51326ee46bb 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -45,7 +45,7 @@ export default class JestJUnitReporter { * @return {undefined} */ onRunComplete(contexts, results) { - if (!process.env.CI || !results.testResults.length) { + if (!process.env.CI || process.env.DISABLE_JUNIT_REPORTER || !results.testResults.length) { return; } diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 16947a97a3d14..25723677390bd 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -34,6 +34,19 @@ module.exports = function (grunt) { return 'Chrome'; } + function pickReporters() { + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + if (process.env.CI && process.env.DISABLE_JUNIT_REPORTER) { + return ['dots']; + } + + if (process.env.CI) { + return ['dots', 'junit']; + } + + return ['progress']; + } + const config = { options: { // base path that will be used to resolve all patterns (eg. files, exclude) @@ -63,14 +76,13 @@ module.exports = function (grunt) { }, }, - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: process.env.CI ? ['dots', 'junit'] : ['progress'], + reporters: pickReporters(), junitReporter: { outputFile: resolve(ROOT, 'target/junit', process.env.JOB || '.', `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}karma.xml`), useBrowserName: false, - nameFormatter: (browser, result) => [...result.suite, result.description].join(' '), - classNameFormatter: (browser, result) => { + nameFormatter: (_, result) => [...result.suite, result.description].join(' '), + classNameFormatter: (_, result) => { const rootSuite = result.suite[0] || result.description; return `Browser Unit Tests.${rootSuite.replace(/\./g, '·')}`; }, diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 1a7a1c973102c..27f73c0b6e20d 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -6,7 +6,7 @@ export TEST_BROWSER_HEADLESS=1 echo " -> Running mocha tests" cd "$XPACK_DIR" -checks-reporter-with-killswitch "X-Pack Mocha" yarn test +checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:browser echo "" echo "" diff --git a/x-pack/gulpfile.js b/x-pack/gulpfile.js index 74e24692f59f6..d3f93c29e3df8 100644 --- a/x-pack/gulpfile.js +++ b/x-pack/gulpfile.js @@ -8,7 +8,7 @@ require('../src/setup_node_env'); const { buildTask } = require('./tasks/build'); const { devTask } = require('./tasks/dev'); -const { testTask, testBrowserTask, testBrowserDevTask, testServerTask } = require('./tasks/test'); +const { testTask, testBrowserTask, testBrowserDevTask } = require('./tasks/test'); const { prepareTask } = require('./tasks/prepare'); // export the tasks that are runnable from the CLI @@ -17,7 +17,6 @@ module.exports = { dev: devTask, prepare: prepareTask, test: testTask, - testserver: testServerTask, testbrowser: testBrowserTask, 'testbrowser-dev': testBrowserDevTask, }; diff --git a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js index a87999873e40f..616aefaf73f62 100644 --- a/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js +++ b/x-pack/legacy/plugins/grokdebugger/server/models/grokdebugger_request/__tests__/grokdebugger_request.js @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { GrokdebuggerRequest } from '../grokdebugger_request'; -describe('grokdebugger_request', () => { +// FAILING: https://github.com/elastic/kibana/issues/51372 +describe.skip('grokdebugger_request', () => { describe('GrokdebuggerRequest', () => { const downstreamRequest = { diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js index c1425de20d146..7b300939bd470 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { addStackStats, getAllStats, handleAllStats } from '../get_all_stats'; -describe('get_all_stats', () => { +// FAILING: https://github.com/elastic/kibana/issues/51371 +describe.skip('get_all_stats', () => { const size = 123; const start = 0; const end = 1; diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js index e3153670ac58f..a0072e52fc7f7 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { getClusterUuids, fetchClusterUuids, handleClusterUuidsResponse } from '../get_cluster_uuids'; -describe('get_cluster_uuids', () => { +// FAILING: https://github.com/elastic/kibana/issues/51371 +describe.skip('get_cluster_uuids', () => { const callWith = sinon.stub(); const size = 123; const server = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js index 195a0d4fdbec4..6d638e50af476 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/cancellation_token.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { CancellationToken } from '../../../../../common/cancellation_token'; -describe('CancellationToken', function () { +// FAILING: https://github.com/elastic/kibana/issues/51373 +describe.skip('CancellationToken', function () { let cancellationToken; beforeEach(function () { cancellationToken = new CancellationToken(); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index 84549d0680ff3..b2e87482b73a1 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -26,6 +26,7 @@ const defaultWorkerOptions = { intervalErrorMultiplier: 10 }; + describe('Worker class', function () { // some of these tests might be a little slow, give them a little extra time this.timeout(10000); @@ -1068,7 +1069,8 @@ describe('Format Job Object', () => { }); }); -describe('Get Doc Path from ES Response', () => { +// FAILING: https://github.com/elastic/kibana/issues/51372 +describe.skip('Get Doc Path from ES Response', () => { it('returns a formatted string after response of an update', function () { const responseMock = { _index: 'foo', diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js index 9a74ba63b8e31..8b5d6f4591ff5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_config.js @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { validateConfig } from '../validate_config'; -describe('Reporting: Validate config', () => { +// FAILING: https://github.com/elastic/kibana/issues/51373 +describe.skip('Reporting: Validate config', () => { const logger = { warning: sinon.spy(), }; diff --git a/x-pack/package.json b/x-pack/package.json index f84db22fe5c40..bc7b220bf81f5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -14,8 +14,7 @@ "test:browser:dev": "gulp testbrowser-dev", "test:browser": "gulp testbrowser", "test:jest": "node scripts/jest", - "test:mocha": "node scripts/mocha", - "test:server": "gulp testserver" + "test:mocha": "node scripts/mocha" }, "kibana": { "build": { @@ -133,7 +132,6 @@ "graphql-codegen-typescript-resolvers": "^0.18.2", "graphql-codegen-typescript-server": "^0.18.2", "gulp": "4.0.2", - "gulp-mocha": "^7.0.2", "hapi": "^17.5.3", "jest": "^24.9.0", "jest-cli": "^24.9.0", diff --git a/x-pack/tasks/test.ts b/x-pack/tasks/test.ts index d26683899ce3f..0767d7479724a 100644 --- a/x-pack/tasks/test.ts +++ b/x-pack/tasks/test.ts @@ -5,35 +5,12 @@ */ import pluginHelpers from '@kbn/plugin-helpers'; -import { createAutoJUnitReporter } from '@kbn/test'; -// @ts-ignore no types available -import mocha from 'gulp-mocha'; import gulp from 'gulp'; import { getEnabledPlugins } from './helpers/flags'; export const testServerTask = async () => { - const pluginIds = await getEnabledPlugins(); - - const testGlobs = ['common/**/__tests__/**/*.js', 'server/**/__tests__/**/*.js']; - - for (const pluginId of pluginIds) { - testGlobs.push( - `legacy/plugins/${pluginId}/__tests__/**/*.js`, - `legacy/plugins/${pluginId}/common/**/__tests__/**/*.js`, - `legacy/plugins/${pluginId}/**/server/**/__tests__/**/*.js` - ); - } - - return gulp.src(testGlobs, { read: false }).pipe( - mocha({ - ui: 'bdd', - require: require.resolve('../../src/setup_node_env'), - reporter: createAutoJUnitReporter({ - reportName: 'X-Pack Mocha Tests', - }), - }) - ); + throw new Error('server mocha tests are now included in the `node scripts/mocha` script'); }; export const testBrowserTask = async () => { @@ -51,4 +28,4 @@ export const testBrowserDevTask = async () => { }); }; -export const testTask = gulp.series(testServerTask, testBrowserTask); +export const testTask = gulp.series(testBrowserTask, testServerTask); diff --git a/yarn.lock b/yarn.lock index 3296fc013c48d..e30abf76145a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9482,11 +9482,6 @@ dargs@^5.1.0: resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" integrity sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk= -dargs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" - integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -11651,21 +11646,6 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/execa/-/execa-2.0.4.tgz#2f5cc589c81db316628627004ea4e37b93391d8e" - integrity sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ== - dependencies: - cross-spawn "^6.0.5" - get-stream "^5.0.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^3.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - execa@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.2.0.tgz#18326b79c7ab7fbd6610fd900c1b9e95fa48f90a" @@ -14225,18 +14205,6 @@ gulp-cli@^2.2.0: v8flags "^3.0.1" yargs "^7.1.0" -gulp-mocha@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/gulp-mocha/-/gulp-mocha-7.0.2.tgz#c7e13d133b3fde96d777e877f90b46225255e408" - integrity sha512-ZXBGN60TXYnFhttr19mfZBOtlHYGx9SvCSc+Kr/m2cMIGloUe176HBPwvPqlakPuQgeTGVRS47NmcdZUereKMQ== - dependencies: - dargs "^7.0.0" - execa "^2.0.4" - mocha "^6.2.0" - plugin-error "^1.0.1" - supports-color "^7.0.0" - through2 "^3.0.1" - gulp-rename@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.4.0.tgz#de1c718e7c4095ae861f7296ef4f3248648240bd" @@ -19352,35 +19320,6 @@ mocha-junit-reporter@^1.23.1: strip-ansi "^4.0.0" xml "^1.0.0" -mocha@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.1.tgz#da941c99437da9bac412097859ff99543969f94c" - integrity sha512-VCcWkLHwk79NYQc8cxhkmI8IigTIhsCwZ6RTxQsqK6go4UvEhzJkYuHm8B2YtlSxcYq2fY+ucr4JBwoD6ci80A== - dependencies: - ansi-colors "3.2.3" - browser-stdout "1.3.1" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" - he "1.2.0" - js-yaml "3.13.1" - log-symbols "2.2.0" - minimatch "3.0.4" - mkdirp "0.5.1" - ms "2.1.1" - node-environment-flags "1.0.5" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.0" - yargs-parser "13.1.1" - yargs-unparser "1.6.0" - mocha@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20" @@ -20193,13 +20132,6 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" - integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== - dependencies: - path-key "^3.0.0" - npm-run-path@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.0.tgz#d644ec1bd0569187d2a52909971023a0a58e8438" From adc11a5d61d4991f9eb85e895a2b89e6e5e68c9a Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 25 Nov 2019 11:44:31 -0700 Subject: [PATCH 057/128] increase the timeout when checking for deprecation log (#51505) * increase the timeout when checking for deprecation log * re-enable the tests --- src/legacy/server/config/__tests__/deprecation_warnings.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js index 3cebc730e66de..0915f7de25b45 100644 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ b/src/legacy/server/config/__tests__/deprecation_warnings.js @@ -25,8 +25,7 @@ const RUN_KBN_SERVER_STARTUP = require.resolve('./fixtures/run_kbn_server_startu const SETUP_NODE_ENV = require.resolve('../../../../setup_node_env'); const SECOND = 1000; -// FLAKY: https://github.com/elastic/kibana/issues/51479 -describe.skip('config/deprecation warnings', function () { +describe('config/deprecation warnings', function () { this.timeout(15 * SECOND); let stdio = ''; @@ -52,9 +51,9 @@ describe.skip('config/deprecation warnings', function () { } }); - // Either time out in 10 seconds, or resolve once the line is in our buffer + // Either time out in 60 seconds, or resolve once the line is in our buffer return Promise.race([ - new Promise((resolve) => setTimeout(resolve, 10000)), + new Promise((resolve) => setTimeout(resolve, 60000)), new Promise((resolve, reject) => { proc.stdout.on('data', (chunk) => { stdio += chunk.toString('utf8'); From 3dcb94db943ba259f7f667f6d7d4e70a05107667 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 25 Nov 2019 13:39:49 -0600 Subject: [PATCH 058/128] [APM] Some miscellaneous client new platform updates (#51482) * Move `setHelpExtension` to plugin start method instead of plugin root * Move `setHelpExtension` to a separate file * Remove 'ui/modules' import * Use new platform capabilities in useUpdateBadgeEffect * Move useUpdateBadgeEffect to a utility function called in start * Add plugins and plugins context to new platform start * Use new platform plugins for KueryBar autocomplete provider * Add types for plugin and rename to ApmPublicPlugin * Add empty setup method to plugin * Move all context providers from App to render method * Remove some unnecessary mocks References #32894. --- .../public/components/app/Home/Home.test.tsx | 1 - .../app/Main/useUpdateBadgeEffect.ts | 31 ------ .../DatePicker/__test__/DatePicker.test.tsx | 2 - .../components/shared/KueryBar/index.tsx | 19 ++-- x-pack/legacy/plugins/apm/public/index.tsx | 35 +----- .../plugins/apm/public/new-platform/index.tsx | 10 +- .../apm/public/new-platform/plugin.tsx | 104 ++++++++++++------ .../public/new-platform/setHelpExtension.ts | 33 ++++++ .../apm/public/new-platform/updateBadge.ts | 27 +++++ 9 files changed, 153 insertions(+), 109 deletions(-) delete mode 100644 x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts create mode 100644 x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts create mode 100644 x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx index 035015c82a0ac..7a23c9f7de842 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx @@ -8,7 +8,6 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Home } from '../Home'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); describe('Home component', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts b/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts deleted file mode 100644 index bb9f581129c5e..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/useUpdateBadgeEffect.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { useEffect } from 'react'; -import { capabilities } from 'ui/capabilities'; -import { useKibanaCore } from '../../../../../observability/public'; - -export const useUpdateBadgeEffect = () => { - const { chrome } = useKibanaCore(); - - useEffect(() => { - const uiCapabilities = capabilities.get(); - chrome.setBadge( - !uiCapabilities.apm.save - ? { - text: i18n.translate('xpack.apm.header.badge.readOnly.text', { - defaultMessage: 'Read only' - }), - tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save' - }), - iconType: 'glasses' - } - : undefined - ); - }, [chrome]); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 881e5975fc81f..05094c59712a9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -18,8 +18,6 @@ import { mount } from 'enzyme'; import { EuiSuperDatePicker } from '@elastic/eui'; import { MemoryRouter } from 'react-router-dom'; -jest.mock('ui/kfetch'); - const mockHistoryPush = jest.spyOn(history, 'push'); const mockRefreshTimeRange = jest.fn(); const MockUrlParamsProvider: React.FC<{ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 66946e5b447f9..24d320505c994 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { npStart } from 'ui/new_platform'; import { StaticIndexPattern } from 'ui/index_patterns'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; @@ -18,16 +17,17 @@ import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { + AutocompleteSuggestion, + AutocompleteProvider +} from '../../../../../../../../src/plugins/data/public'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; +import { usePlugins } from '../../../new-platform/plugin'; const Container = styled.div` margin-bottom: 10px; `; -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); - interface State { suggestions: AutocompleteSuggestion[]; isLoadingSuggestions: boolean; @@ -45,9 +45,9 @@ function getSuggestions( query: string, selectionStart: number, indexPattern: StaticIndexPattern, - boolFilter: unknown + boolFilter: unknown, + autocompleteProvider?: AutocompleteProvider ) { - const autocompleteProvider = getAutocompleteProvider('kuery'); if (!autocompleteProvider) { return []; } @@ -74,6 +74,8 @@ export function KueryBar() { }); const { urlParams } = useUrlParams(); const location = useLocation(); + const { data } = usePlugins(); + const autocompleteProvider = data.autocomplete.getProvider('kuery'); let currentRequestCheck; @@ -108,7 +110,8 @@ export function KueryBar() { inputValue, selectionStart, indexPattern, - boolFilter + boolFilter, + autocompleteProvider ) ) .filter(suggestion => !startsWith(suggestion.text, 'span.')) diff --git a/x-pack/legacy/plugins/apm/public/index.tsx b/x-pack/legacy/plugins/apm/public/index.tsx index 8fd3cb0893dea..db14e1c520020 100644 --- a/x-pack/legacy/plugins/apm/public/index.tsx +++ b/x-pack/legacy/plugins/apm/public/index.tsx @@ -6,43 +6,18 @@ import { npStart } from 'ui/new_platform'; import 'react-vis/dist/style.css'; +import { PluginInitializerContext } from 'kibana/public'; import 'ui/autoload/all'; import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import url from 'url'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; import { plugin } from './new-platform'; import { REACT_APP_ROOT_ID } from './new-platform/plugin'; import './style/global_overrides.css'; import template from './templates/index.html'; -const { core } = npStart; - -// render APM feedback link in global help menu -core.chrome.setHelpExtension({ - appName: i18n.translate('xpack.apm.feedbackMenu.appName', { - defaultMessage: 'APM' - }), - links: [ - { - linkType: 'discuss', - href: 'https://discuss.elastic.co/c/apm' - }, - { - linkType: 'custom', - href: url.format({ - pathname: core.http.basePath.prepend('/app/kibana'), - hash: '/management/elasticsearch/upgrade_assistant' - }), - content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { - defaultMessage: 'Upgrade assistant' - }) - } - ] -}); +const { core, plugins } = npStart; +// This will be moved to core.application.register when the new platform +// migration is complete. // @ts-ignore chrome.setRootTemplate(template); @@ -57,5 +32,5 @@ const checkForRoot = () => { }); }; checkForRoot().then(() => { - plugin().start(core); + plugin({} as PluginInitializerContext).start(core, plugins); }); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx index cb4cc2a845a4c..9dce4bcdd828c 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin } from './plugin'; +import { PluginInitializer } from '../../../../../../src/core/public'; +import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; -export function plugin() { - return new Plugin(); -} +export const plugin: PluginInitializer< + ApmPluginSetup, + ApmPluginStart +> = _core => new ApmPlugin(); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index ac4aca4c795b7..b5986610d3048 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext, createContext } from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; -import { LegacyCoreStart } from 'src/core/public'; +import { + CoreStart, + LegacyCoreStart, + Plugin, + CoreSetup +} from '../../../../../../src/core/public'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { KibanaCoreContextProvider } from '../../../observability/public'; import { history } from '../utils/history'; import { LocationProvider } from '../context/LocationContext'; @@ -19,9 +25,10 @@ import { LicenseProvider } from '../context/LicenseContext'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { routes } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { useUpdateBadgeEffect } from '../components/app/Main/useUpdateBadgeEffect'; import { MatchedRouteProvider } from '../context/MatchedRouteContext'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; +import { setHelpExtension } from './setHelpExtension'; +import { setReadonlyBadge } from './updateBadge'; export const REACT_APP_ROOT_ID = 'react-apm-root'; @@ -31,41 +38,70 @@ const MainContainer = styled.main` `; const App = () => { - useUpdateBadgeEffect(); - return ( - - - - - - - - - {routes.map((route, i) => ( - - ))} - - - - - - + + + + + {routes.map((route, i) => ( + + ))} + + ); }; -export class Plugin { - public start(core: LegacyCoreStart) { - const { i18n } = core; +export type ApmPluginSetup = void; +export type ApmPluginStart = void; +export type ApmPluginSetupDeps = {}; // eslint-disable-line @typescript-eslint/consistent-type-definitions + +export interface ApmPluginStartDeps { + data: DataPublicPluginStart; +} + +const PluginsContext = createContext({} as ApmPluginStartDeps); + +export function usePlugins() { + return useContext(PluginsContext); +} + +export class ApmPlugin + implements + Plugin< + ApmPluginSetup, + ApmPluginStart, + ApmPluginSetupDeps, + ApmPluginStartDeps + > { + // Take the DOM element as the constructor, so we can mount the app. + public setup(_core: CoreSetup, _plugins: ApmPluginSetupDeps) {} + + public start(core: CoreStart, plugins: ApmPluginStartDeps) { + const i18nCore = core.i18n; + + // render APM feedback link in global help menu + setHelpExtension(core); + setReadonlyBadge(core); + ReactDOM.render( - - - - - - - - + + + + + + + + + + + + + + + + + + , document.getElementById(REACT_APP_ROOT_ID) ); @@ -76,4 +112,6 @@ export class Plugin { console.log('Error fetching static index pattern', e); }); } + + public stop() {} } diff --git a/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts b/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts new file mode 100644 index 0000000000000..1a3394651b2ff --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import url from 'url'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +export function setHelpExtension({ chrome, http }: CoreStart) { + chrome.setHelpExtension({ + appName: i18n.translate('xpack.apm.feedbackMenu.appName', { + defaultMessage: 'APM' + }), + links: [ + { + linkType: 'discuss', + href: 'https://discuss.elastic.co/c/apm' + }, + { + linkType: 'custom', + href: url.format({ + pathname: http.basePath.prepend('/app/kibana'), + hash: '/management/elasticsearch/upgrade_assistant' + }), + content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { + defaultMessage: 'Upgrade assistant' + }) + } + ] + }); +} diff --git a/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts b/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts new file mode 100644 index 0000000000000..b3e29bb891c23 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +export function setReadonlyBadge({ application, chrome }: CoreStart) { + const canSave = application.capabilities.apm.save; + const { setBadge } = chrome; + + setBadge( + !canSave + ? { + text: i18n.translate('xpack.apm.header.badge.readOnly.text', { + defaultMessage: 'Read only' + }), + tooltip: i18n.translate('xpack.apm.header.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save' + }), + iconType: 'glasses' + } + : undefined + ); +} From 33e85370e311fd8797a75016127873cda21caf94 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 25 Nov 2019 13:58:38 -0700 Subject: [PATCH 059/128] [Maps] refactor feature id assignment (#51317) * [Maps] refactor feature id assignment * add test case for falsy id value * review feedback --- .../maps/public/elasticsearch_geo_utils.js | 11 +-- .../public/elasticsearch_geo_utils.test.js | 6 +- .../client_file_source/geojson_file_source.js | 16 +--- .../ems_file_source/ems_file_source.js | 4 +- .../es_geo_grid_source/convert_to_geojson.js | 7 +- .../es_pew_pew_source/convert_to_lines.js | 6 +- .../kibana_regionmap_source.js | 5 +- .../public/layers/util/assign_feature_ids.js | 57 +++++++++++++ .../layers/util/assign_feature_ids.test.js | 83 +++++++++++++++++++ .../maps/public/layers/vector_layer.js | 46 ++-------- 10 files changed, 162 insertions(+), 79 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index ef2819f1f372c..4b04251edd94a 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -11,7 +11,6 @@ import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS, - FEATURE_ID_PROPERTY_NAME, GEO_JSON_TYPE, POLYGON_COORDINATES_EXTERIOR_INDEX, LON_INDEX, @@ -81,12 +80,10 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { features.push({ type: 'Feature', geometry: tmpGeometriesAccumulator[j], - properties: { - ...properties, - // _id is not unique across Kibana index pattern. Multiple ES indices could have _id collisions - // Need to prefix with _index to guarantee uniqueness - [FEATURE_ID_PROPERTY_NAME]: `${properties._index}:${properties._id}:${j}` - } + // _id is not unique across Kibana index pattern. Multiple ES indices could have _id collisions + // Need to prefix with _index to guarantee uniqueness + id: `${properties._index}:${properties._id}:${j}`, + properties, }); } } diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js index 0b84b4c32f4ac..45aa2af15eb9d 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -74,8 +74,8 @@ describe('hitsToGeoJson', () => { coordinates: [100, 20], type: 'Point', }, + id: 'index1:doc1:0', properties: { - __kbn__feature_id__: 'index1:doc1:0', _id: 'doc1', _index: 'index1', }, @@ -139,8 +139,8 @@ describe('hitsToGeoJson', () => { coordinates: [100, 20], type: 'Point', }, + id: 'index1:doc1:0', properties: { - __kbn__feature_id__: 'index1:doc1:0', _id: 'doc1', _index: 'index1', myField: 8 @@ -152,8 +152,8 @@ describe('hitsToGeoJson', () => { coordinates: [110, 30], type: 'Point', }, + id: 'index1:doc1:1', properties: { - __kbn__feature_id__: 'index1:doc1:1', _id: 'doc1', _index: 'index1', myField: 8 diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 11a02d58a9198..920253d15eaee 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -10,7 +10,6 @@ import { ES_GEO_FIELD_TYPE, GEOJSON_FILE, ES_SIZE_LIMIT, - FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; import { ESSearchSource } from '../es_search_source'; @@ -137,21 +136,8 @@ export class GeojsonFileSource extends AbstractVectorSource { } async getGeoJsonWithMeta() { - const copiedPropsFeatures = this._descriptor.__featureCollection.features.map((feature, index) => { - const properties = feature.properties ? { ...feature.properties } : {}; - properties[FEATURE_ID_PROPERTY_NAME] = index; - return { - type: 'Feature', - geometry: feature.geometry, - properties, - }; - }); - return { - data: { - type: 'FeatureCollection', - features: copiedPropsFeatures - }, + data: this._descriptor.__featureCollection, meta: {} }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index b2e04f56e5718..fcd52683b70ff 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -7,7 +7,7 @@ import { AbstractVectorSource } from '../vector_source'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; -import { EMS_FILE, FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants'; +import { EMS_FILE, FIELD_ORIGIN } from '../../../../common/constants'; import { getEMSClient } from '../../../meta'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; @@ -94,7 +94,7 @@ export class EMSFileSource extends AbstractVectorSource { return field.type === 'id'; }); featureCollection.features.forEach((feature, index) => { - feature.properties[FEATURE_ID_PROPERTY_NAME] = emsIdField + feature.id = emsIdField ? feature.properties[emsIdField.id] : index; }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js index c83f12ce992ff..d26bfd8bbeacb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js @@ -6,7 +6,7 @@ import { RENDER_AS } from './render_as'; import { getTileBoundingBox } from './geo_tile_utils'; -import { EMPTY_FEATURE_COLLECTION, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; +import { EMPTY_FEATURE_COLLECTION } from '../../../../common/constants'; export function convertToGeoJson({ table, renderAs }) { @@ -34,9 +34,7 @@ export function convertToGeoJson({ table, renderAs }) { return; } - const properties = { - [FEATURE_ID_PROPERTY_NAME]: gridKey - }; + const properties = {}; metricColumns.forEach(metricColumn => { properties[metricColumn.aggConfig.id] = row[metricColumn.id]; }); @@ -49,6 +47,7 @@ export function convertToGeoJson({ table, renderAs }) { geocentroidColumn, renderAs, }), + id: gridKey, properties }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js index c334776e6c4e8..ae9435dc42c69 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js @@ -6,8 +6,6 @@ import _ from 'lodash'; -import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants'; - const LAT_INDEX = 0; const LON_INDEX = 1; @@ -47,10 +45,10 @@ export function convertToLines(esResponse) { type: 'LineString', coordinates: [[sourceCentroid.location.lon, sourceCentroid.location.lat], dest] }, + id: `${dest.join()},${key}`, properties: { - [FEATURE_ID_PROPERTY_NAME]: `${dest.join()},${key}`, ...rest - } + }, }); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index ffccb18a69192..e29887edcf7d9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -10,7 +10,7 @@ import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { FEATURE_ID_PROPERTY_NAME, FIELD_ORIGIN } from '../../../../common/constants'; +import { FIELD_ORIGIN } from '../../../../common/constants'; import { KibanaRegionField } from '../../fields/kibana_region_field'; export class KibanaRegionmapSource extends AbstractVectorSource { @@ -91,9 +91,6 @@ export class KibanaRegionmapSource extends AbstractVectorSource { featureCollectionPath: vectorFileMeta.meta.feature_collection_path, fetchUrl: vectorFileMeta.url }); - featureCollection.features.forEach((feature, index) => { - feature.properties[FEATURE_ID_PROPERTY_NAME] = index; - }); return { data: featureCollection }; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js new file mode 100644 index 0000000000000..2c0d08f86cfc0 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; + +let idCounter = 0; + +function generateNumericalId() { + const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; + idCounter = newId + 1; + return newId; +} + +export function assignFeatureIds(featureCollection) { + + // wrt https://github.com/elastic/kibana/issues/39317 + // In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. + // This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. + // This is a work-around to avoid hitting such a worst-case + // This was tested as a suitable work-around for mapbox-gl 0.54 + // The core issue itself is likely related to https://github.com/mapbox/mapbox-gl-js/issues/6086 + + // This only shuffles the id-assignment, _not_ the features in the collection + // The reason for this is that we do not want to modify the feature-ordering, which is the responsiblity of the VectorSource#. + const ids = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const id = generateNumericalId(); + ids.push(id); + } + + const randomizedIds = _.shuffle(ids); + const features = []; + for (let i = 0; i < featureCollection.features.length; i++) { + const numericId = randomizedIds[i]; + const feature = featureCollection.features[i]; + features.push({ + type: 'Feature', + geometry: feature.geometry, // do not copy geometry, this object can be massive + properties: { + // preserve feature id provided by source so features can be referenced across fetches + [FEATURE_ID_PROPERTY_NAME]: feature.id == null ? numericId : feature.id, + // create new object for properties so original is not polluted with kibana internal props + ...feature.properties, + }, + id: numericId, // Mapbox feature state id, must be integer + }); + } + + return { + type: 'FeatureCollection', + features + }; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js new file mode 100644 index 0000000000000..0678070f568a2 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/assign_feature_ids.test.js @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { assignFeatureIds } from './assign_feature_ids'; +import { FEATURE_ID_PROPERTY_NAME } from '../../../common/constants'; + +const featureId = 'myFeature1'; + +test('should provide unique id when feature.id is not provided', () => { + const featureCollection = { + features: [ + { + properties: {} + }, + { + properties: {} + }, + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + const feature2 = updatedFeatureCollection.features[1]; + expect(typeof feature1.id).toBe('number'); + expect(typeof feature2.id).toBe('number'); + expect(feature1.id).toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.id).not.toBe(feature2.id); +}); + +test('should preserve feature id when provided', () => { + const featureCollection = { + features: [ + { + id: featureId, + properties: {} + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(typeof feature1.id).toBe('number'); + expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); +}); + +test('should preserve feature id for falsy value', () => { + const featureCollection = { + features: [ + { + id: 0, + properties: {} + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(typeof feature1.id).toBe('number'); + expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]); + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(0); +}); + +test('should not modify original feature properties', () => { + const featureProperties = {}; + const featureCollection = { + features: [ + { + id: featureId, + properties: featureProperties + } + ] + }; + + const updatedFeatureCollection = assignFeatureIds(featureCollection); + const feature1 = updatedFeatureCollection.features[0]; + expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId); + expect(featureProperties).not.toHaveProperty(FEATURE_ID_PROPERTY_NAME); +}); + diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 362c7bfd72540..e6b07b983d898 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -23,6 +23,7 @@ import { isRefreshOnlyQuery } from './util/is_refresh_only_query'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; +import { assignFeatureIds } from './util/assign_feature_ids'; const VISIBILITY_FILTER_CLAUSE = ['all', [ @@ -59,16 +60,6 @@ const POINT_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, ] ]; - -let idCounter = 0; - -function generateNumericalId() { - const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0; - idCounter = newId + 1; - return newId; -} - - export class VectorLayer extends AbstractLayer { static type = LAYER_TYPE.VECTOR; @@ -478,15 +469,15 @@ export class VectorLayer extends AbstractLayer { try { startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters); const layerName = await this.getDisplayName(); - const { data: featureCollection, meta } = + const { data: sourceFeatureCollection, meta } = await this._source.getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback.bind(null, requestToken) ); - this._assignIdsToFeatures(featureCollection); - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, featureCollection, meta); + const layerFeatureCollection = assignFeatureIds(sourceFeatureCollection); + stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, layerFeatureCollection, meta); return { refreshed: true, - featureCollection: featureCollection + featureCollection: layerFeatureCollection }; } catch (error) { if (!(error instanceof DataRequestAbortError)) { @@ -498,31 +489,6 @@ export class VectorLayer extends AbstractLayer { } } - _assignIdsToFeatures(featureCollection) { - - //wrt https://github.com/elastic/kibana/issues/39317 - //In constrained resource environments, mapbox-gl may throw a stackoverflow error due to hitting the browser's recursion limit. This crashes Kibana. - //This error is thrown in mapbox-gl's quicksort implementation, when it is sorting all the features by id. - //This is a work-around to avoid hitting such a worst-case - //This was tested as a suitable work-around for mapbox-gl 0.54 - //The core issue itself is likely related to https://github.com/mapbox/mapbox-gl-js/issues/6086 - - //This only shuffles the id-assignment, _not_ the features in the collection - //The reason for this is that we do not want to modify the feature-ordering, which is the responsiblity of the VectorSource#. - const ids = []; - for (let i = 0; i < featureCollection.features.length; i++) { - const id = generateNumericalId(); - ids.push(id); - } - - const randomizedIds = _.shuffle(ids); - for (let i = 0; i < featureCollection.features.length; i++) { - const id = randomizedIds[i]; - const feature = featureCollection.features[i]; - feature.id = id; // Mapbox feature state id, must be integer - } - } - async syncData(syncContext) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; @@ -534,8 +500,8 @@ export class VectorLayer extends AbstractLayer { } const joinStates = await this._syncJoins(syncContext); - await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); + await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); } _getSourceFeatureCollection() { From 24df2a3716c5be2b560d14e96c46eafdb49f8929 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 25 Nov 2019 14:54:28 -0700 Subject: [PATCH 060/128] add serverMocha config for flaky job (#51508) * add serverMocha config for flaky job * fix typo * no reason to setup everything over and over, just call scripts/mocha * force CI_GROUP param for testing * define local CI_GROUP_PARAM that can be assigned alternate values temporarily * add additional metadata to job description * add workerNumber param to worker block * use kibanaPipeline.getPostBuildWorker to define wrapper function * use kibanaPipeline specific bash function * revert changes made for debugging --- .ci/Jenkinsfile_flaky | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 8ad02b7162b6a..669395564db44 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -3,10 +3,13 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -// Looks like 'oss:ciGroup:1' or 'oss:firefoxSmoke' -def JOB_PARTS = params.CI_GROUP.split(':') +def CI_GROUP_PARAM = params.CI_GROUP + +// Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke', or 'all:serverMocha' +def JOB_PARTS = CI_GROUP_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' def JOB = JOB_PARTS[1] +def NEED_BUILD = JOB != 'serverMocha' def CI_GROUP = JOB_PARTS.size() > 2 ? JOB_PARTS[2] : '' def EXECUTIONS = params.NUMBER_EXECUTIONS.toInteger() def AGENT_COUNT = getAgentCount(EXECUTIONS) @@ -31,13 +34,15 @@ stage("Kibana Pipeline") { print "Agent ${agentNumberInside} - ${agentExecutions} executions" kibanaPipeline.withWorkers('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + if (NEED_BUILD) { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } - } else { - kibanaPipeline.buildXpack() } }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() } @@ -61,7 +66,17 @@ stage("Kibana Pipeline") { def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { - if (job == 'firefoxSmoke') { + if (job == 'serverMocha') { + return kibanaPipeline.getPostBuildWorker('serverMocha', { + kibanaPipeline.bash( + """ + source src/dev/ci_setup/setup_env.sh + node scripts/mocha + """, + "run `node scripts/mocha`" + ) + }) + } else if (job == 'firefoxSmoke') { return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) } else if(job == 'visualRegression') { return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) From edf2751480f0edad82df4dde992fbf24a2ce20cb Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 25 Nov 2019 15:20:26 -0700 Subject: [PATCH 061/128] [SIEM][Detection Engine] Makes input and output indexes optional for the REST API (#51551) * Initial logic added, just need to change the input and output parameters to be optional now * Removed the input and output indexes as they are now optional in all the examples * Added a check for attributes and unit tests for the get_input_output * Removed more output_signals stuff from scripts * Updated the README docs * Removed input and output index for the conversion script * Removed the scripts doing a replacement of the output index so we can rely on defaults more and to make things simplier * Added ability to convert everything to an ndjson for importer, flipped enabled to false by default with the script --- .../convert_saved_search_to_signals.js | 21 +- .../plugins/siem/server/kibana.index.ts | 3 +- .../server/lib/detection_engine/README.md | 7 + .../alerts/get_input_output_index.test.ts | 286 ++++++++++++++++++ .../alerts/get_input_output_index.ts | 72 +++++ .../alerts/signals_alert_type.ts | 31 +- .../detection_engine/routes/schemas.test.ts | 8 +- .../lib/detection_engine/routes/schemas.ts | 4 +- .../detection_engine/scripts/post_signal.sh | 3 +- .../scripts/post_x_signals.sh | 1 - .../scripts/signals/root_or_admin_1.json | 2 - .../scripts/signals/root_or_admin_10.json | 2 - .../scripts/signals/root_or_admin_2.json | 2 - .../scripts/signals/root_or_admin_3.json | 2 - .../scripts/signals/root_or_admin_4.json | 2 - .../scripts/signals/root_or_admin_5.json | 2 - .../scripts/signals/root_or_admin_6.json | 2 - .../scripts/signals/root_or_admin_7.json | 2 - .../scripts/signals/root_or_admin_8.json | 2 - .../scripts/signals/root_or_admin_9.json | 2 - .../signals/root_or_admin_filter_9998.json | 2 - .../signals/root_or_admin_filter_9999.json | 2 - .../scripts/signals/root_or_admin_meta.json | 2 - .../signals/root_or_admin_saved_query_1.json | 2 - .../signals/root_or_admin_saved_query_2.json | 2 - .../signals/root_or_admin_saved_query_3.json | 2 - .../signals/root_or_admin_update_1.json | 2 - .../signals/root_or_admin_update_2.json | 2 - .../scripts/signals/watch_longmont.json | 2 - .../detection_engine/scripts/update_signal.sh | 3 +- 30 files changed, 416 insertions(+), 61 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js index 597d93a44210b..263a2a59de31f 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js @@ -34,9 +34,17 @@ const TYPE = 'query'; const FROM = 'now-6m'; const TO = 'now'; const IMMUTABLE = true; -const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; -const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; const RISK_SCORE = 50; +const ENABLED = false; +let allSignals = ''; +const allSignalsNdJson = 'all_rules.ndjson'; + +// For converting, if you want to use these instead of rely on the defaults then +// comment these in and use them for the script. Otherwise this is commented out +// so we can utilize the defaults of input and output which are based on saved objects +// of siem:defaultIndex and siem:defaultSignalsIndex +// const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; +// const OUTPUT_INDEX = process.env.SIGNALS_INDEX || '.siem-signals'; const walk = dir => { const list = fs.readdirSync(dir); @@ -124,7 +132,6 @@ async function main() { risk_score: RISK_SCORE, description: description || title, immutable: IMMUTABLE, - index: INDEX, interval: INTERVAL, name: title, severity: SEVERITY, @@ -134,16 +141,22 @@ async function main() { query, language, filters: filter, - output_index: OUTPUT_INDEX, + enabled: ENABLED, + // comment these in if you want to use these for input output, otherwise + // with these two commented out, we will use the default saved objects from spaces. + // index: INDEX, + // output_index: OUTPUT_INDEX, }; fs.writeFileSync( `${outputDir}/${fileToWrite}.json`, JSON.stringify(outputMessage, null, 2) ); + allSignals += `${JSON.stringify(outputMessage)}\n`; } } ); + fs.writeFileSync(`${outputDir}/${allSignalsNdJson}`, allSignals); } if (require.main === module) { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index c79b2651c11cb..a92bca064dab9 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -32,7 +32,8 @@ export const initServerWithKibana = ( mode: EnvironmentMode ) => { if (kbnServer.plugins.alerting != null) { - const type = signalsAlertType({ logger }); + const version = kbnServer.config().get('pkg.version'); + const type = signalsAlertType({ logger, version }); if (isAlertExecutor(type)) { kbnServer.plugins.alerting.setup.registerType(type); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 0a0439a9ace1b..5d9d87a1cbc2f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -104,6 +104,13 @@ You should also see the SIEM detect the feature flags and start the API endpoint server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints ``` +Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same +value as you did with the environment variable of SIGNALS_INDEX, which should be `.siem-signals-${your user id}` + +``` +.siem-signals-${your user id} +``` + Open a terminal and go into the scripts folder `cd kibana/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts` and run: ```sh diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts new file mode 100644 index 0000000000000..07eb7c885b443 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { + DEFAULT_SIGNALS_INDEX_KEY, + DEFAULT_INDEX_KEY, + DEFAULT_SIGNALS_INDEX, +} from '../../../../common/constants'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { getInputOutputIndex, getOutputIndex, getInputIndex } from './get_input_output_index'; +import { defaultIndexPattern } from '../../../../default_index_pattern'; + +describe('get_input_output_index', () => { + let savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + let servicesMock: AlertServices = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + + beforeAll(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + servicesMock = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + }); + + describe('getInputOutputIndex', () => { + test('Returns inputIndex as is if inputIndex and outputIndex are both passed in', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + 'test-output-index' + ); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns outputIndex as is if inputIndex and outputIndex are both passed in', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + 'test-output-index' + ); + expect(outputIndex).toEqual('test-output-index'); + }); + + test('Returns inputIndex as is if inputIndex is defined but outputIndex is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + null + ); + expect(inputIndex).toEqual(['test-input-index-1']); + }); + + test('Returns outputIndex as is if inputIndex is null but outputIndex is defined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + null, + 'test-output-index' + ); + expect(outputIndex).toEqual('test-output-index'); + }); + + test('Returns a saved object outputIndex if both passed in are undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', + }, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + undefined + ); + expect(outputIndex).toEqual('.signals-test-index'); + }); + + test('Returns a saved object outputIndex if passed in outputIndex is undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.signals-test-index', + }, + })); + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + ['test-input-index-1'], + undefined + ); + expect(outputIndex).toEqual('.signals-test-index'); + }); + + test('Returns a saved object outputIndex default from constants if both passed in input and configuration are null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: null, + }, + })); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const { outputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('Returns a saved object outputIndex default from constants if both passed in input and configuration are missing', async () => { + const { outputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + undefined + ); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('Returns a saved object inputIndex if passed in inputIndex and outputIndex are undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex if passed in inputIndex is undefined', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['configured-index-1', 'configured-index-2'], + }, + })); + const { inputIndex } = await getInputOutputIndex( + servicesMock, + '8.0.0', + undefined, + 'output-index-1' + ); + expect(inputIndex).toEqual(['configured-index-1', 'configured-index-2']); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', null, null); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('Returns a saved object inputIndex default from constants if both passed in inputIndex and configuration attributes is missing', async () => { + const { inputIndex } = await getInputOutputIndex(servicesMock, '8.0.0', undefined, undefined); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + }); + + describe('getOutputIndex', () => { + test('test output index is returned when passed in as is', async () => { + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex('output-index-1', mockConfiguration); + expect(outputIndex).toEqual('output-index-1'); + }); + + test('configured output index is returned when output index is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: '.siem-test-signals', + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual('.siem-test-signals'); + }); + + test('output index from constants is returned when output index is null and so is the configuration', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_SIGNALS_INDEX_KEY]: null, + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('output index from constants is returned when output index is null and configuration is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + + test('output index from constants is returned when output index is null and attributes is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const outputIndex = getOutputIndex(null, mockConfiguration); + expect(outputIndex).toEqual(DEFAULT_SIGNALS_INDEX); + }); + }); + + describe('getInputIndex', () => { + test('test input index is returned when passed in as is', async () => { + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(['input-index-1'], mockConfiguration); + expect(inputIndex).toEqual(['input-index-1']); + }); + + test('configured input index is returned when input index is null', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: ['input-index-1', 'input-index-2'], + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(['input-index-1', 'input-index-2']); + }); + + test('input index from constants is returned when input index is null and so is the configuration', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + [DEFAULT_INDEX_KEY]: null, + }, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('input index from constants is returned when input index is null and configuration is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: {}, + })); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + + test('input index from constants is returned when input index is null and attributes is missing', async () => { + savedObjectsClient.get = jest.fn().mockImplementation(() => ({})); + const mockConfiguration = await savedObjectsClient.get('config', '8.0.0'); + const inputIndex = getInputIndex(null, mockConfiguration); + expect(inputIndex).toEqual(defaultIndexPattern); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts new file mode 100644 index 0000000000000..567ab27976d8d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { defaultIndexPattern } from '../../../../default_index_pattern'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_SIGNALS_INDEX_KEY, + DEFAULT_SIGNALS_INDEX, +} from '../../../../common/constants'; + +interface IndexObjectAttributes extends SavedObjectAttributes { + [DEFAULT_INDEX_KEY]: string[]; + [DEFAULT_SIGNALS_INDEX_KEY]: string; +} + +export const getInputIndex = ( + inputIndex: string[] | undefined | null, + configuration: SavedObject +): string[] => { + if (inputIndex != null) { + return inputIndex; + } else { + if (configuration.attributes != null && configuration.attributes[DEFAULT_INDEX_KEY] != null) { + return configuration.attributes[DEFAULT_INDEX_KEY]; + } else { + return defaultIndexPattern; + } + } +}; + +export const getOutputIndex = ( + outputIndex: string | undefined | null, + configuration: SavedObject +): string => { + if (outputIndex != null) { + return outputIndex; + } else { + if ( + configuration.attributes != null && + configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY] != null + ) { + return configuration.attributes[DEFAULT_SIGNALS_INDEX_KEY]; + } else { + return DEFAULT_SIGNALS_INDEX; + } + } +}; + +export const getInputOutputIndex = async ( + services: AlertServices, + version: string, + inputIndex: string[] | null | undefined, + outputIndex: string | null | undefined +): Promise<{ + inputIndex: string[]; + outputIndex: string; +}> => { + if (inputIndex != null && outputIndex != null) { + return { inputIndex, outputIndex }; + } else { + const configuration = await services.savedObjectsClient.get('config', version); + return { + inputIndex: getInputIndex(inputIndex, configuration), + outputIndex: getOutputIndex(outputIndex, configuration), + }; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts index 8308bca68e9af..dfc779329d3b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts @@ -12,8 +12,15 @@ import { buildEventsSearchQuery } from './build_events_query'; import { searchAfterAndBulkIndex } from './utils'; import { SignalAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; +import { getInputOutputIndex } from './get_input_output_index'; -export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTypeDefinition => { +export const signalsAlertType = ({ + logger, + version, +}: { + logger: Logger; + version: string; +}): SignalAlertTypeDefinition => { return { id: SIGNALS_ID, name: 'SIEM Signals', @@ -26,9 +33,9 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp filter: schema.nullable(schema.object({}, { allowUnknowns: true })), ruleId: schema.string(), immutable: schema.boolean({ defaultValue: false }), - index: schema.arrayOf(schema.string()), + index: schema.nullable(schema.arrayOf(schema.string())), language: schema.nullable(schema.string()), - outputIndex: schema.string(), + outputIndex: schema.nullable(schema.string()), savedId: schema.nullable(schema.string()), meta: schema.nullable(schema.object({}, { allowUnknowns: true })), query: schema.nullable(schema.string()), @@ -70,6 +77,12 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp const searchAfterSize = size ? size : 1000; + const { inputIndex, outputIndex: signalsIndex } = await getInputOutputIndex( + services, + version, + index, + outputIndex + ); const esFilter = await getFilter({ type, filter, @@ -78,11 +91,11 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp query, savedId, services, - index, + index: inputIndex, }); const noReIndex = buildEventsSearchQuery({ - index, + index: inputIndex, from, to, filter: esFilter, @@ -98,7 +111,11 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp const noReIndexResult = await services.callCluster('search', noReIndex); if (noReIndexResult.hits.total.value !== 0) { logger.info( - `Total signals found from signal rule "id: ${alertId}", "ruleId: ${ruleId}": ${noReIndexResult.hits.total.value}` + `Found ${ + noReIndexResult.hits.total.value + } signals from the indexes of "${inputIndex.join( + ', ' + )}" using signal rule "id: ${alertId}", "ruleId: ${ruleId}", pushing signals to index ${signalsIndex}` ); } @@ -108,7 +125,7 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp services, logger, id: alertId, - signalsIndex: outputIndex, + signalsIndex, name, createdBy, updatedBy, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 5e5f37ca8a080..6639dc6a3dfd6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -174,7 +174,7 @@ describe('schemas', () => { ).toBeTruthy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does not validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( createSignalsSchema.validate>({ rule_id: 'rule-1', @@ -190,7 +190,7 @@ describe('schemas', () => { query: 'some query', language: 'kuery', }).error - ).toBeTruthy(); + ).toBeFalsy(); }); test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { @@ -213,7 +213,7 @@ describe('schemas', () => { ).toBeFalsy(); }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does not validate', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( createSignalsSchema.validate>({ rule_id: 'rule-1', @@ -228,7 +228,7 @@ describe('schemas', () => { filter: {}, risk_score: 50, }).error - ).toBeTruthy(); + ).toBeFalsy(); }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index fa773b684eb5d..210ce5ca9fdce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -68,7 +68,7 @@ export const createSignalsSchema = Joi.object({ from: from.required(), rule_id, immutable: immutable.default(false), - index: index.required(), + index, interval: interval.default('5m'), query: Joi.when('type', { is: 'query', @@ -95,7 +95,7 @@ export const createSignalsSchema = Joi.object({ otherwise: Joi.forbidden(), }), }), - output_index: output_index.required(), + output_index, saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.required(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh index 8455e7d27ad47..b8bd0e0e0361f 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh @@ -18,13 +18,12 @@ SIGNALS=(${@:-./signals/root_or_admin_1.json}) for SIGNAL in "${SIGNALS[@]}" do { [ -e "$SIGNAL" ] || continue - POST=$(jq '.output_index=env.SIGNALS_INDEX' $SIGNAL) curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d "$POST" \ + -d @${SIGNAL} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh index 8362c576ff554..abb2111a91c1b 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh @@ -24,7 +24,6 @@ do { --data "{ \"rule_id\": \"${i}\", \"risk_score\": \"50\", - \"output_index\": \"${SIGNALS_INDEX}"\", \"description\": \"Detecting root and admin users\", \"index\": [\"auditbeat-*\", \"filebeat-*\", \"packetbeat-*\", \"winlogbeat-*\"], \"interval\": \"24h\", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json index 8586b29c29886..b00a5929d9ef1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json @@ -2,10 +2,8 @@ "rule_id": "rule-1", "risk_score": 1, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json index 85bc09f0f9f85..657439104e306 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json @@ -1,13 +1,11 @@ { "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "references": ["http://www.example.com", "https://ww.example.com"] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json index 8f2d826ae9ae1..137cf7eedbccf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json @@ -2,9 +2,7 @@ "rule_id": "rule-2", "risk_score": 2, "description": "Detecting root and admin users over a long period of time", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", - "output_index": ".siem-signals", "name": "Detect Root/Admin Users over a long period of time", "severity": "high", "type": "query", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json index 10bfc2e0d74a3..b9160c95621ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json @@ -2,10 +2,8 @@ "rule_id": "rule-3", "risk_score": 3, "description": "Detecting root and admin users as an empty set", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-16y", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json index 18cfb808007b3..364e7f00c9571 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json @@ -2,10 +2,8 @@ "rule_id": "rule-4", "risk_score": 4, "description": "Detecting root and admin users with lucene", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json index a445a839a8228..eb7f2ae03b64b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json @@ -2,10 +2,8 @@ "rule_id": "rule-5", "risk_score": 5, "description": "Detecting root and admin users over 24 hours on windows", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-24h", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json index 6e2f7a3f82a50..94f30bc9f92df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json @@ -2,10 +2,8 @@ "rule_id": "rule-6", "risk_score": 6, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-24h", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json index 9da8a11861a4d..81ec19a4fd0ef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json @@ -2,14 +2,12 @@ "rule_id": "rule-7", "risk_score": 7, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-24h", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "lucene", "filters": [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json index ad8c651bb3ec8..de24263c6af5c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json @@ -2,14 +2,12 @@ "rule_id": "rule-8", "risk_score": 8, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "enabled": false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json index 3658e6e4e9428..9bf2b1abf5f90 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json @@ -2,14 +2,12 @@ "rule_id": "rule-9", "risk_score": 9, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "query", "from": "now-6m", "to": "now", - "output_index": ".siem-signals", "query": "user.name: root or user.name: admin", "language": "kuery", "enabled": false, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json index db53ea07fe34b..2381e9e259c07 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json @@ -2,12 +2,10 @@ "rule_id": "rule-9999", "risk_score": 100, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "filter", - "output_index": ".siem-signals", "from": "now-6m", "to": "now", "filter": { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json index e6cc661af404c..ee8fe1fc93fb3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json @@ -2,12 +2,10 @@ "rule_id": "rule-9999", "risk_score": 100, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", "type": "filter", - "output_index": ".siem-signals", "from": "now-6m", "to": "now", "filter": { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json index 266ceeba15d47..ed8f2e5745bea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json @@ -2,10 +2,8 @@ "rule_id": "rule-meta-data", "risk_score": 1, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", - "output_index": ".siem-signals", "severity": "high", "type": "query", "from": "now-6m", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json index d5559ebe23bdb..721644acd989d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-1", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json index e272273d817d2..b733b6bb8c592 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-2", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json index 9fc2c32c7daf1..df1b37f19bf29 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json @@ -2,11 +2,9 @@ "rule_id": "saved-query-3", "risk_score": 5, "description": "Detecting root and admin users", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "output_index": ".siem-signals", "type": "saved_query", "from": "now-6m", "to": "now", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json index 42834141a72fd..09ddfb1c34a92 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json @@ -2,12 +2,10 @@ "rule_id": "rule-1", "risk_score": 98, "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", "type": "query", - "output_index": ".siem-signals", "from": "now-6m", "to": "now-5m", "query": "user.name: root", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json index 4c03f041e6e2f..8a3c765519ef3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json @@ -2,7 +2,6 @@ "rule_id": "rule-1", "risk_score": 78, "description": "Changed Description of only detecting root user", - "index": ["auditbeat-*"], "interval": "50m", "name": "A different name", "severity": "high", @@ -12,7 +11,6 @@ "immutable": true, "tags": ["some other tag for you"], "to": "now-5m", - "output_index": ".siem-signals", "query": "user.name: root", "language": "kuery", "references": ["https://update1.example.com", "https://update2.example.com"] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json index cfb5fab8b8493..a43398bd6876a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json @@ -2,12 +2,10 @@ "rule_id": "rule-longmont", "risk_score": 5, "description": "Detect Longmont activity", - "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", "name": "Detect Longmont activity", "severity": "high", "type": "query", - "output_index": ".siem-signals", "from": "now-1y", "to": "now", "query": "user.name: root or user.name: admin", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh index 6984e7b4c810b..04541e1df1fa1 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh @@ -18,13 +18,12 @@ SIGNALS=(${@:-./signals/root_or_admin_update_1.json}) for SIGNAL in "${SIGNALS[@]}" do { [ -e "$SIGNAL" ] || continue - POST=$(jq '.output_index=env.SIGNALS_INDEX' $SIGNAL) curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d "$POST" \ + -d @${SIGNAL} \ | jq .; } & done From fff493e54ecd063c738509f3e41099c225d469d7 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 25 Nov 2019 16:29:35 -0600 Subject: [PATCH 062/128] Build bundles for all new platform plugins (#51525) --- ...ibana-plugin-server.pluginsservicesetup.md | 3 +- ...ver.pluginsservicesetup.uipluginconfigs.md | 11 --- ...in-server.pluginsservicesetup.uiplugins.md | 3 +- src/core/server/legacy/legacy_service.test.ts | 6 +- src/core/server/legacy/legacy_service.ts | 1 - .../server/plugins/plugins_service.mock.ts | 4 +- .../server/plugins/plugins_service.test.ts | 78 ++++++++++++++----- src/core/server/plugins/plugins_service.ts | 35 ++++++--- .../server/plugins/plugins_system.test.ts | 13 +--- src/core/server/plugins/plugins_system.ts | 21 +---- src/core/server/plugins/types.ts | 9 +-- src/core/server/server.api.md | 7 +- src/legacy/server/kbn_server.d.ts | 1 - src/legacy/ui/ui_render/ui_render_mixin.js | 2 +- src/optimize/base_optimizer.js | 8 +- src/optimize/index.js | 2 +- src/optimize/watch/optmzr_role.js | 2 +- 17 files changed, 113 insertions(+), 93 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md index 36d803ddea618..248726e26f393 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.md @@ -16,6 +16,5 @@ export interface PluginsServiceSetup | Property | Type | Description | | --- | --- | --- | | [contracts](./kibana-plugin-server.pluginsservicesetup.contracts.md) | Map<PluginName, unknown> | | -| [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) | Map<PluginName, Observable<unknown>> | | -| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
    public: Map<PluginName, DiscoveredPlugin>;
    internal: Map<PluginName, DiscoveredPluginInternal>;
    } | | +| [uiPlugins](./kibana-plugin-server.pluginsservicesetup.uiplugins.md) | {
    internal: Map<PluginName, InternalPluginInfo>;
    public: Map<PluginName, DiscoveredPlugin>;
    browserConfigs: Map<PluginName, Observable<unknown>>;
    } | | diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md deleted file mode 100644 index 4bd57b873043e..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) > [uiPluginConfigs](./kibana-plugin-server.pluginsservicesetup.uipluginconfigs.md) - -## PluginsServiceSetup.uiPluginConfigs property - -Signature: - -```typescript -uiPluginConfigs: Map>; -``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md index fa286dfb59092..7c47304cb9bf6 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsservicesetup.uiplugins.md @@ -8,7 +8,8 @@ ```typescript uiPlugins: { + internal: Map; public: Map; - internal: Map; + browserConfigs: Map>; }; ``` diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 1240518422e2f..030caa8324521 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -41,7 +41,7 @@ import { configServiceMock } from '../config/config_service.mock'; import { BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; -import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; +import { DiscoveredPlugin } from '../plugins'; import { KibanaMigrator } from '../saved_objects/migrations'; import { ISavedObjectsClientProvider } from '../saved_objects'; @@ -84,9 +84,9 @@ beforeEach(() => { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), + internal: new Map([['plugin-id', { entryPointPath: 'path/to/plugin/public' }]]), + browserConfigs: new Map(), }, - uiPluginConfigs: new Map(), }, }, plugins: { 'plugin-id': 'plugin-value' }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index e86e6cde6e927..99963ad9ce3e8 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -278,7 +278,6 @@ export class LegacyService implements CoreService { hapiServer: setupDeps.core.http.server, kibanaMigrator: startDeps.core.savedObjects.migrator, uiPlugins: setupDeps.core.plugins.uiPlugins, - uiPluginConfigs: setupDeps.core.plugins.uiPluginConfigs, elasticsearch: setupDeps.core.elasticsearch, uiSettings: setupDeps.core.uiSettings, savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index e3be8fbb98309..8d3c6a8c909a2 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -30,10 +30,10 @@ const createServiceMock = () => { mocked.setup.mockResolvedValue({ contracts: new Map(), uiPlugins: { - public: new Map(), + browserConfigs: new Map(), internal: new Map(), + public: new Map(), }, - uiPluginConfigs: new Map(), }); mocked.start.mockResolvedValue({ contracts: new Map() }); return mocked; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index da6d1d5a010e7..7e55faa43360e 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -33,11 +33,12 @@ import { PluginsService } from './plugins_service'; import { PluginsSystem } from './plugins_system'; import { config } from './plugins_config'; import { take } from 'rxjs/operators'; -import { DiscoveredPluginInternal } from './types'; +import { DiscoveredPlugin } from './types'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; let pluginsService: PluginsService; +let config$: BehaviorSubject; let configService: ConfigService; let coreId: symbol; let env: Env; @@ -107,11 +108,10 @@ describe('PluginsService', () => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - configService = new ConfigService( - new BehaviorSubject(new ObjectToConfigAdapter({ plugins: { initialize: true } })), - env, - logger + config$ = new BehaviorSubject( + new ObjectToConfigAdapter({ plugins: { initialize: true } }) ); + configService = new ConfigService(config$, env, logger); await configService.setSchema(config.path, config.schema); pluginsService = new PluginsService({ coreId, env, logger, configService }); @@ -198,7 +198,7 @@ describe('PluginsService', () => { .mockImplementation(path => Promise.resolve(!path.includes('disabled'))); mockPluginSystem.setupPlugins.mockResolvedValue(new Map()); - mockPluginSystem.uiPlugins.mockReturnValue({ public: new Map(), internal: new Map() }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); mockDiscover.mockReturnValue({ error$: from([]), @@ -390,11 +390,10 @@ describe('PluginsService', () => { }); describe('#generateUiPluginsConfigs()', () => { - const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPluginInternal] => [ + const pluginToDiscoveredEntry = (plugin: PluginWrapper): [string, DiscoveredPlugin] => [ plugin.name, { id: plugin.name, - path: plugin.path, configPath: plugin.manifest.configPath, requiredPlugins: [], optionalPlugins: [], @@ -427,15 +426,14 @@ describe('PluginsService', () => { error$: from([]), plugin$: from([plugin]), }); - mockPluginSystem.uiPlugins.mockReturnValue({ - public: new Map([pluginToDiscoveredEntry(plugin)]), - internal: new Map([pluginToDiscoveredEntry(plugin)]), - }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); await pluginsService.discover(); - const { uiPluginConfigs } = await pluginsService.setup(setupDeps); + const { + uiPlugins: { browserConfigs }, + } = await pluginsService.setup(setupDeps); - const uiConfig$ = uiPluginConfigs.get('plugin-with-expose'); + const uiConfig$ = browserConfigs.get('plugin-with-expose'); expect(uiConfig$).toBeDefined(); const uiConfig = await uiConfig$!.pipe(take(1)).toPromise(); @@ -468,15 +466,55 @@ describe('PluginsService', () => { error$: from([]), plugin$: from([plugin]), }); - mockPluginSystem.uiPlugins.mockReturnValue({ - public: new Map([pluginToDiscoveredEntry(plugin)]), - internal: new Map([pluginToDiscoveredEntry(plugin)]), - }); + mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)])); await pluginsService.discover(); - const { uiPluginConfigs } = await pluginsService.setup(setupDeps); + const { + uiPlugins: { browserConfigs }, + } = await pluginsService.setup(setupDeps); - expect([...uiPluginConfigs.entries()]).toHaveLength(0); + expect([...browserConfigs.entries()]).toHaveLength(0); + }); + }); + + describe('#setup()', () => { + describe('uiPlugins.internal', () => { + it('includes disabled plugins', async () => { + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('plugin-1', { + path: 'path-1', + version: 'some-version', + configPath: 'plugin1', + }), + createPlugin('plugin-2', { + path: 'path-2', + version: 'some-version', + configPath: 'plugin2', + }), + ]), + }); + + mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + + config$.next( + new ObjectToConfigAdapter({ plugins: { initialize: true }, plugin1: { enabled: false } }) + ); + + await pluginsService.discover(); + const { uiPlugins } = await pluginsService.setup({} as any); + expect(uiPlugins.internal).toMatchInlineSnapshot(` + Map { + "plugin-1" => Object { + "entryPointPath": "path-1/public", + }, + "plugin-2" => Object { + "entryPointPath": "path-2/public", + }, + } + `); + }); }); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 79c9489a8b4c0..4c73c2a304dc4 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -25,12 +25,7 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { PluginWrapper } from './plugin'; -import { - DiscoveredPlugin, - DiscoveredPluginInternal, - PluginConfigDescriptor, - PluginName, -} from './types'; +import { DiscoveredPlugin, PluginConfigDescriptor, PluginName, InternalPluginInfo } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; import { InternalCoreSetup } from '../internal_types'; @@ -41,10 +36,22 @@ import { pick } from '../../utils'; export interface PluginsServiceSetup { contracts: Map; uiPlugins: { + /** + * Paths to all discovered ui plugin entrypoints on the filesystem, even if + * disabled. + */ + internal: Map; + + /** + * Information needed by client-side to load plugins and wire dependencies. + */ public: Map; - internal: Map; + + /** + * Configuration for plugins to be exposed to the client-side. + */ + browserConfigs: Map>; }; - uiPluginConfigs: Map>; } /** @public */ @@ -65,6 +72,7 @@ export class PluginsService implements CoreService; private readonly pluginConfigDescriptors = new Map(); + private readonly uiPluginInternalInfo = new Map(); constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('plugins-service'); @@ -103,8 +111,11 @@ export class PluginsService implements CoreService { expect(thirdPluginToRun.setup).toHaveBeenCalledTimes(1); }); -test('`uiPlugins` returns empty Maps before plugins are added', async () => { - expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(` - Object { - "internal": Map {}, - "public": Map {}, - } - `); +test('`uiPlugins` returns empty Map before plugins are added', async () => { + expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(`Map {}`); }); test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { @@ -351,7 +346,7 @@ test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { pluginsSystem.addPlugin(plugin); }); - expect([...pluginsSystem.uiPlugins().internal.keys()]).toMatchInlineSnapshot(` + expect([...pluginsSystem.uiPlugins().keys()]).toMatchInlineSnapshot(` Array [ "order-0", "order-1", @@ -380,7 +375,7 @@ test('`uiPlugins` returns only ui plugin dependencies', async () => { pluginsSystem.addPlugin(plugin); }); - const plugin = pluginsSystem.uiPlugins().internal.get('ui-plugin')!; + const plugin = pluginsSystem.uiPlugins().get('ui-plugin')!; expect(plugin.requiredPlugins).toEqual(['req-ui']); expect(plugin.optionalPlugins).toEqual(['opt-ui']); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 34acb66d4e931..f437b51e5b07a 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -17,12 +17,10 @@ * under the License. */ -import { pick } from 'lodash'; - import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName, PluginOpaqueId } from './types'; +import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; @@ -158,33 +156,22 @@ export class PluginsSystem { const uiPluginNames = [...this.getTopologicallySortedPluginNames().keys()].filter( pluginName => this.plugins.get(pluginName)!.includesUiPlugin ); - const internal = new Map( + const publicPlugins = new Map( uiPluginNames.map(pluginName => { const plugin = this.plugins.get(pluginName)!; return [ pluginName, { id: pluginName, - path: plugin.path, configPath: plugin.manifest.configPath, requiredPlugins: plugin.manifest.requiredPlugins.filter(p => uiPluginNames.includes(p)), optionalPlugins: plugin.manifest.optionalPlugins.filter(p => uiPluginNames.includes(p)), }, - ] as [PluginName, DiscoveredPluginInternal]; + ]; }) ); - const publicPlugins = new Map( - [...internal.entries()].map( - ([pluginName, plugin]) => - [ - pluginName, - pick(plugin, ['id', 'configPath', 'requiredPlugins', 'optionalPlugins']), - ] as [PluginName, DiscoveredPlugin] - ) - ); - - return { public: publicPlugins, internal }; + return publicPlugins; } /** diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 17704ce687b92..fd487d9fe00aa 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -169,15 +169,14 @@ export interface DiscoveredPlugin { } /** - * An extended `DiscoveredPlugin` that exposes more sensitive information. Should never - * be exposed to client-side code. * @internal */ -export interface DiscoveredPluginInternal extends DiscoveredPlugin { +export interface InternalPluginInfo { /** - * Path on the filesystem where plugin was loaded from. + * Path to the client-side entrypoint file to be used to build the client-side + * bundle for a plugin. */ - readonly path: string; + readonly entryPointPath: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7ecb9053a4bcf..066f79bfd38f3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1014,11 +1014,10 @@ export interface PluginsServiceSetup { // (undocumented) contracts: Map; // (undocumented) - uiPluginConfigs: Map>; - // (undocumented) uiPlugins: { + internal: Map; public: Map; - internal: Map; + browserConfigs: Map>; }; } @@ -1628,6 +1627,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugins_service.ts:45:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts ``` diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 7399f2d08508f..9cc4e30d4252d 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -107,7 +107,6 @@ export default class KbnServer { __internals: { hapiServer: LegacyServiceSetupDeps['core']['http']['server']; uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins']; - uiPluginConfigs: LegacyServiceSetupDeps['core']['plugins']['uiPluginConfigs']; elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch']; uiSettings: LegacyServiceSetupDeps['core']['uiSettings']; kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator']; diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index c0885cd5d3d13..763167c6b5ccf 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -234,7 +234,7 @@ export function uiRenderMixin(kbnServer, server, config) { // Get the list of new platform plugins. // Convert the Map into an array of objects so it is JSON serializable and order is preserved. - const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPluginConfigs; + const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPlugins.browserConfigs; const uiPlugins = await Promise.all([ ...kbnServer.newPlatform.__internals.uiPlugins.public.entries(), ].map(async ([id, plugin]) => { diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 28eb448d12d82..2eaf4c1d6e882 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -61,7 +61,7 @@ export default class BaseOptimizer { constructor(opts) { this.logWithMetadata = opts.logWithMetadata || (() => null); this.uiBundles = opts.uiBundles; - this.discoveredPlugins = opts.discoveredPlugins; + this.newPlatformPluginInfo = opts.newPlatformPluginInfo; this.profile = opts.profile || false; this.workers = opts.workers; @@ -551,9 +551,9 @@ export default class BaseOptimizer { _getDiscoveredPluginEntryPoints() { // New platform plugin entry points - return [...this.discoveredPlugins.entries()] - .reduce((entryPoints, [pluginId, plugin]) => { - entryPoints[`plugin/${pluginId}`] = `${plugin.path}/public`; + return [...this.newPlatformPluginInfo.entries()] + .reduce((entryPoints, [pluginId, pluginInfo]) => { + entryPoints[`plugin/${pluginId}`] = pluginInfo.entryPointPath; return entryPoints; }, {}); } diff --git a/src/optimize/index.js b/src/optimize/index.js index 9789e7abc2f9d..0960f9ecb10b6 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -66,7 +66,7 @@ export default async (kbnServer, server, config) => { const optimizer = new FsOptimizer({ logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), uiBundles, - discoveredPlugins: newPlatform.__internals.uiPlugins.internal, + newPlatformPluginInfo: newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index 16be840b3ca0e..9fbeceb578615 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -30,7 +30,7 @@ export default async (kbnServer, kibanaHapiServer, config) => { const watchOptimizer = new WatchOptimizer({ logWithMetadata, uiBundles: kbnServer.uiBundles, - discoveredPlugins: kbnServer.newPlatform.__internals.uiPlugins.internal, + newPlatformPluginInfo: kbnServer.newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), From aeb082e7c2ff2b2f24ee844a760894ef5c339085 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Mon, 25 Nov 2019 17:31:01 -0500 Subject: [PATCH 063/128] Resolving the a11y heading issue with edit/create user (#51538) --- .../views/management/edit_user/components/edit_user_page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx index 282ce4eea160c..91f5f048adc6d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx @@ -374,7 +374,7 @@ class EditUserPageUI extends Component { -

    +

    {isNewUser ? ( { values={{ userName: user.username }} /> )} -

    +
    {reserved && ( From 1da0dc8aec2f7e58031704daf219e24617ceac3f Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 25 Nov 2019 16:31:42 -0600 Subject: [PATCH 064/128] Use real appBasePath in legacy AppService shim (#51353) --- src/legacy/ui/public/new_platform/new_platform.test.ts | 6 ++++-- src/legacy/ui/public/new_platform/new_platform.ts | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index cbdaccd65f94b..e5d5cd0a87776 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -18,13 +18,15 @@ */ import { setRootControllerMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__ } from './new_platform'; +import { legacyAppRegister, __reset__, __setup__ } from './new_platform'; +import { coreMock } from '../../../../core/public/mocks'; describe('ui/new_platform', () => { describe('legacyAppRegister', () => { beforeEach(() => { setRootControllerMock.mockReset(); __reset__(); + __setup__(coreMock.createSetup({ basePath: '/test/base/path' }) as any, {} as any); }); const registerApp = () => { @@ -59,7 +61,7 @@ describe('ui/new_platform', () => { controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { element: elementMock[0], - appBasePath: '', + appBasePath: '/test/base/path/app/test', }); }); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index acf1191852dc8..36bfbcc7d5d46 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -111,7 +111,10 @@ export const legacyAppRegister = (app: App) => { // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { - const unmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + const unmount = await app.mount( + { core: npStart.core }, + { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) } + ); $scope.$on('$destroy', () => { unmount(); }); From fb476b26ebc9bceaa7e3d8f10a58505b7121b309 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 25 Nov 2019 15:44:49 -0700 Subject: [PATCH 065/128] skip flaky suite (#51669) --- .../apps/machine_learning/anomaly_detection/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index ba307a24cd739..2b76bce544f6d 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -6,7 +6,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { - describe('anomaly detection', function() { + // FLAKY: https://github.com/elastic/kibana/issues/51669 + describe.skip('anomaly detection', function() { loadTestFile(require.resolve('./single_metric_job')); loadTestFile(require.resolve('./multi_metric_job')); loadTestFile(require.resolve('./population_job')); From 71ed8567fb1133d2ea83f3899551ce3a6baffd48 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Mon, 25 Nov 2019 19:10:33 -0500 Subject: [PATCH 066/128] [SIEM] Signal _id generation to prevent duplicates / signal drops and addition of new constants (#51200) * updates how signal document id is generated through the result of sha256 hash of rule id plus source event id to prevent duplicate ids. Also adds constant for search_after result size to default to 100, and adds default maxSignals constant currently set to 100 signals. * updates bulk index to use bulk create for better duplicate handling, updates how each id is generated for each document inserted into signals index, updates error handling and logging statements * fix type check errors * allows docs with the same id / index but different versions to be created in signals index * adds tests for new id generation mechanism in signals index, optimizes search_after result sizing if total results in a signle results page is less than maxSignals, updates relevant tests * adds some comments * fixes failing tests after removing size from rest response * updates tests to push down generated uuids into test data, updates tests to ensure signal ids match static hash to ensure underlying changes to how the hash is generated are tested as well. * adds tests to ensure generated id is no larger than 512 bytes * replaces splice with slice in unit tests * updates id generator to include rule_id --- .../legacy/plugins/siem/common/constants.ts | 2 + .../alerts/__mocks__/es_results.ts | 130 ++++++-- .../detection_engine/alerts/create_signals.ts | 1 - .../alerts/signals_alert_type.ts | 21 +- .../lib/detection_engine/alerts/types.ts | 42 ++- .../lib/detection_engine/alerts/utils.test.ts | 287 +++++++++++++----- .../lib/detection_engine/alerts/utils.ts | 96 ++++-- .../routes/create_signals_route.ts | 2 - .../lib/detection_engine/routes/schemas.ts | 3 +- .../routes/update_signals_route.ts | 2 - .../lib/detection_engine/routes/utils.test.ts | 9 - .../lib/detection_engine/routes/utils.ts | 1 - 12 files changed, 443 insertions(+), 153 deletions(-) diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index f01493cec869e..e5d1fc83dac26 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -17,6 +17,8 @@ export const DEFAULT_SIEM_TIME_RANGE = 'siem:timeDefaults'; export const DEFAULT_SIEM_REFRESH_INTERVAL = 'siem:refreshIntervalDefaults'; export const DEFAULT_SIGNALS_INDEX_KEY = 'siem:defaultSignalsIndex'; export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; +export const DEFAULT_MAX_SIGNALS = 100; +export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore'; export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; export const DEFAULT_SCALE_DATE_FORMAT = 'dateFormat:scaled'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 0a70a7342b2dd..7d3b51c071c09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -30,33 +30,43 @@ export const sampleSignalAlertParams = ( filters: undefined, savedId: undefined, meta: undefined, - size: 1000, }); -export const sampleDocNoSortId: SignalSourceHit = { +export const sampleDocNoSortId = (someUuid: string): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, _version: 1, - _id: 'someFakeId', + _id: someUuid, _source: { someKey: 'someValue', '@timestamp': 'someTimeStamp', }, -}; +}); + +export const sampleDocNoSortIdNoVersion = (someUuid: string): SignalSourceHit => ({ + _index: 'myFakeSignalIndex', + _type: 'doc', + _score: 100, + _id: someUuid, + _source: { + someKey: 'someValue', + '@timestamp': 'someTimeStamp', + }, +}); -export const sampleDocWithSortId: SignalSourceHit = { +export const sampleDocWithSortId = (someUuid: string): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, _version: 1, - _id: 'someFakeId', + _id: someUuid, _source: { someKey: 'someValue', '@timestamp': 'someTimeStamp', }, sort: ['1234567891111'], -}; +}); export const sampleEmptyDocSearchResults: SignalSearchResponse = { took: 10, @@ -74,7 +84,61 @@ export const sampleEmptyDocSearchResults: SignalSearchResponse = { }, }; -export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { +export const sampleBulkCreateDuplicateResult = { + took: 60, + errors: true, + items: [ + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + _version: 1, + result: 'created', + _shards: { + total: 2, + successful: 1, + failed: 0, + }, + _seq_no: 1, + _primary_term: 1, + status: 201, + }, + }, + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + status: 409, + error: { + type: 'version_conflict_engine_exception', + reason: '[4]: version conflict, document already exists (current version [1])', + index_uuid: 'cXmq4Rt3RGGswDTTwZFzvA', + shard: '0', + index: 'test', + }, + }, + }, + { + create: { + _index: 'test', + _type: '_doc', + _id: '4', + status: 409, + error: { + type: 'version_conflict_engine_exception', + reason: '[4]: version conflict, document already exists (current version [1])', + index_uuid: 'cXmq4Rt3RGGswDTTwZFzvA', + shard: '0', + index: 'test', + }, + }, + }, + ], +}; + +export const sampleDocSearchResultsNoSortId = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -88,13 +152,35 @@ export const sampleDocSearchResultsNoSortId: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocNoSortId, + ...sampleDocNoSortId(someUuid), }, ], }, -}; +}); + +export const sampleDocSearchResultsNoSortIdNoVersion = ( + someUuid: string +): SignalSearchResponse => ({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: 100, + max_score: 100, + hits: [ + { + ...sampleDocNoSortIdNoVersion(someUuid), + }, + ], + }, +}); -export const sampleDocSearchResultsNoSortIdNoHits: SignalSearchResponse = { +export const sampleDocSearchResultsNoSortIdNoHits = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -108,13 +194,17 @@ export const sampleDocSearchResultsNoSortIdNoHits: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocNoSortId, + ...sampleDocNoSortId(someUuid), }, ], }, -}; +}); -export const repeatedSearchResultsWithSortId = (repeat: number) => ({ +export const repeatedSearchResultsWithSortId = ( + total: number, + pageSize: number, + guids: string[] +) => ({ took: 10, timed_out: false, _shards: { @@ -124,15 +214,15 @@ export const repeatedSearchResultsWithSortId = (repeat: number) => ({ skipped: 0, }, hits: { - total: repeat, + total, max_score: 100, - hits: Array.from({ length: repeat }).map(x => ({ - ...sampleDocWithSortId, + hits: Array.from({ length: pageSize }).map((x, index) => ({ + ...sampleDocWithSortId(guids[index]), })), }, }); -export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { +export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -146,10 +236,10 @@ export const sampleDocSearchResultsWithSortId: SignalSearchResponse = { max_score: 100, hits: [ { - ...sampleDocWithSortId, + ...sampleDocWithSortId(someUuid), }, ], }, -}; +}); export const sampleSignalId = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts index 420f995431423..8770282356cf5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts @@ -29,7 +29,6 @@ export const createSignals = async ({ outputIndex, name, severity, - size, tags, to, type, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts index dfc779329d3b2..69eb3eb665060 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts @@ -6,10 +6,14 @@ import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; -import { SIGNALS_ID } from '../../../../common/constants'; +import { + SIGNALS_ID, + DEFAULT_MAX_SIGNALS, + DEFAULT_SEARCH_AFTER_PAGE_SIZE, +} from '../../../../common/constants'; import { buildEventsSearchQuery } from './build_events_query'; -import { searchAfterAndBulkIndex } from './utils'; +import { searchAfterAndBulkCreate } from './utils'; import { SignalAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; import { getInputOutputIndex } from './get_input_output_index'; @@ -40,14 +44,13 @@ export const signalsAlertType = ({ meta: schema.nullable(schema.object({}, { allowUnknowns: true })), query: schema.nullable(schema.string()), filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - maxSignals: schema.number({ defaultValue: 10000 }), + maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), riskScore: schema.number(), severity: schema.string(), tags: schema.arrayOf(schema.string(), { defaultValue: [] }), to: schema.string(), type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), - size: schema.maybe(schema.number()), }), }, async executor({ alertId, services, params }) { @@ -63,7 +66,6 @@ export const signalsAlertType = ({ query, to, type, - size, } = params; // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 @@ -75,7 +77,11 @@ export const signalsAlertType = ({ const interval: string = savedObject.attributes.interval; const enabled: boolean = savedObject.attributes.enabled; - const searchAfterSize = size ? size : 1000; + // set searchAfter page size to be the lesser of default page size or maxSignals. + const searchAfterSize = + DEFAULT_SEARCH_AFTER_PAGE_SIZE <= params.maxSignals + ? DEFAULT_SEARCH_AFTER_PAGE_SIZE + : params.maxSignals; const { inputIndex, outputIndex: signalsIndex } = await getInputOutputIndex( services, @@ -119,7 +125,7 @@ export const signalsAlertType = ({ ); } - const bulkIndexResult = await searchAfterAndBulkIndex({ + const bulkIndexResult = await searchAfterAndBulkCreate({ someResult: noReIndexResult, signalParams: params, services, @@ -131,6 +137,7 @@ export const signalsAlertType = ({ updatedBy, interval, enabled, + pageSize: searchAfterSize, }); if (bulkIndexResult) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 9c6e1f99c672b..29eb7872f163d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -42,7 +42,6 @@ export interface SignalAlertParams { savedId: string | undefined | null; meta: Record | undefined | null; severity: string; - size: number | undefined | null; tags: string[]; to: string; type: 'filter' | 'query' | 'saved_query'; @@ -173,7 +172,46 @@ export interface SignalSource { export interface BulkResponse { took: number; errors: boolean; - items: unknown[]; + items: [ + { + create: { + _index: string; + _type?: string; + _id: string; + _version: number; + result?: string; + _shards?: { + total: number; + successful: number; + failed: number; + }; + _seq_no?: number; + _primary_term?: number; + status: number; + error?: { + type: string; + reason: string; + index_uuid?: string; + shard: string; + index: string; + }; + }; + } + ]; +} + +export interface MGetResponse { + docs: GetResponse[]; +} +export interface GetResponse { + _index: string; + _type: string; + _id: string; + _version: number; + _seq_no: number; + _primary_term: number; + found: boolean; + _source: SearchTypes; } export type SignalSearchResponse = SearchResponse; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index bc147fa1dae07..4aac425c7f80f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -3,23 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import uuid from 'uuid'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { Logger } from '../../../../../../../../src/core/server'; import { buildBulkBody, - singleBulkIndex, + generateId, + singleBulkCreate, singleSearchAfter, - searchAfterAndBulkIndex, + searchAfterAndBulkCreate, } from './utils'; import { sampleDocNoSortId, sampleSignalAlertParams, sampleDocSearchResultsNoSortId, sampleDocSearchResultsNoSortIdNoHits, + sampleDocSearchResultsNoSortIdNoVersion, sampleDocSearchResultsWithSortId, sampleEmptyDocSearchResults, repeatedSearchResultsWithSortId, + sampleBulkCreateDuplicateResult, sampleSignalId, } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; @@ -46,9 +51,10 @@ describe('utils', () => { }); describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { + const fakeUuid = uuid.v4(); const sampleParams = sampleSignalAlertParams(undefined); const fakeSignalSourceHit = buildBulkBody({ - doc: sampleDocNoSortId, + doc: sampleDocNoSortId(fakeUuid), signalParams: sampleParams, id: sampleSignalId, name: 'rule-name', @@ -59,11 +65,13 @@ describe('utils', () => { }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; + if (fakeSignalSourceHit.signal.parent) { + delete fakeSignalSourceHit.signal.parent.id; + } expect(fakeSignalSourceHit).toEqual({ someKey: 'someValue', signal: { parent: { - id: 'someFakeId', type: 'event', index: 'myFakeSignalIndex', depth: 1, @@ -88,7 +96,6 @@ describe('utils', () => { severity: 'high', tags: ['some fake tag'], type: 'query', - size: 1000, status: 'open', to: 'now', enabled: true, @@ -99,8 +106,114 @@ describe('utils', () => { }); }); }); - describe('singleBulkIndex', () => { - test('create successful bulk index', async () => { + describe('singleBulkCreate', () => { + describe('create signal id gereateId', () => { + test('two docs with same index, id, and version should have same id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const generatedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId); + expect(firstHash).toEqual(generatedHash); + expect(secondHash).toEqual(generatedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + }); + test('two docs with different index, id, and version should have different id', () => { + const findex = 'myfakeindex'; + const findex2 = 'mysecondfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'mysecondfakeindexsomefakeid1rule-1' + const secondGeneratedHash = + 'a852941273f805ffe9006e574601acc8ae1148d6c0b3f7f8c4785cba8f6b768a'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex2, fid, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, different id, and same version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const fid2 = 'somefakeid2'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'myfakeindexsomefakeid21rule-1' + const secondGeneratedHash = + '7d33faea18159fd010c4b79890620e8b12cdc88ec1d370149d0e5552ce860255'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid2, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, same id, and different version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const version2 = '2'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid2rule-1' + const secondGeneratedHash = + 'f016f3071fa9df9221d2fb2ba92389d4d388a4347c6ec7a4012c01cb1c640a40'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version2, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('Ensure generated id is less than 512 bytes, even for really really long strings', () => { + const longIndexName = + 'myfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const firstHash = generateId(longIndexName, fid, version, ruleId); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + }); + test('two docs with same index, same id, same version number, and different rule ids should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const ruleId2 = 'rule-2'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = + '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid1rule-2' + const secondGeneratedHash = + '1eb04f997086f8b3b143d4d9b18ac178c4a7423f71a5dad9ba8b9e92603c6863'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId2); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + }); + test('create successful bulk create', async () => { + const fakeUuid = uuid.v4(); const sampleParams = sampleSignalAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ @@ -112,8 +225,36 @@ describe('utils', () => { }, ], }); - const successfulSingleBulkIndex = await singleBulkIndex({ - someResult: sampleSearchResult, + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), + signalParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleSignalId, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + expect(successfulsingleBulkCreate).toEqual(true); + }); + test('create successful bulk create with docs with no versioning', async () => { + const fakeUuid = uuid.v4(); + const sampleParams = sampleSignalAlertParams(undefined); + const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; + mockService.callCluster.mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -125,13 +266,13 @@ describe('utils', () => { interval: '5m', enabled: true, }); - expect(successfulSingleBulkIndex).toEqual(true); + expect(successfulsingleBulkCreate).toEqual(true); }); - test('create unsuccessful bulk index due to empty search results', async () => { + test('create unsuccessful bulk create due to empty search results', async () => { const sampleParams = sampleSignalAlertParams(undefined); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); - const successfulSingleBulkIndex = await singleBulkIndex({ + const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult, signalParams: sampleParams, services: mockService, @@ -144,18 +285,15 @@ describe('utils', () => { interval: '5m', enabled: true, }); - expect(successfulSingleBulkIndex).toEqual(true); + expect(successfulsingleBulkCreate).toEqual(true); }); - test('create unsuccessful bulk index due to bulk index errors', async () => { - // need a sample search result, sample signal params, mock service, mock logger + test('create successful bulk create when bulk create has errors', async () => { + const fakeUuid = uuid.v4(); const sampleParams = sampleSignalAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockReturnValue({ - took: 100, - errors: true, - }); - const successfulSingleBulkIndex = await singleBulkIndex({ - someResult: sampleSearchResult, + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(fakeUuid), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -168,7 +306,7 @@ describe('utils', () => { enabled: true, }); expect(mockLogger.error).toHaveBeenCalled(); - expect(successfulSingleBulkIndex).toEqual(false); + expect(successfulsingleBulkCreate).toEqual(true); }); }); describe('singleSearchAfter', () => { @@ -182,6 +320,7 @@ describe('utils', () => { signalParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }) ).rejects.toThrow('Attempted to search after with empty sort id'); }); @@ -194,6 +333,7 @@ describe('utils', () => { signalParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }); expect(searchAfterResult).toEqual(sampleDocSearchResultsWithSortId); }); @@ -209,14 +349,15 @@ describe('utils', () => { signalParams: sampleParams, services: mockService, logger: mockLogger, + pageSize: 1, }) ).rejects.toThrow('Fake Error'); }); }); - describe('searchAfterAndBulkIndex', () => { + describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { const sampleParams = sampleSignalAlertParams(undefined); - const result = await searchAfterAndBulkIndex({ + const result = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults, signalParams: sampleParams, services: mockService, @@ -228,12 +369,14 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleSignalAlertParams(30); + const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -244,7 +387,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(4)) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) .mockReturnValueOnce({ took: 100, errors: false, @@ -254,7 +397,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(4)) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) .mockReturnValueOnce({ took: 100, errors: false, @@ -264,8 +407,8 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -276,18 +419,17 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(result).toEqual(true); }); - test('if unsuccessful first bulk index', async () => { + test('if unsuccessful first bulk create', async () => { + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); const sampleParams = sampleSignalAlertParams(10); - mockService.callCluster.mockReturnValue({ - took: 100, - errors: true, // will cause singleBulkIndex to return false - }); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -298,13 +440,14 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); }); - test('if unsuccessful iteration of searchAfterAndBulkIndex due to empty sort ids', async () => { + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { const sampleParams = sampleSignalAlertParams(undefined); - + const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -314,8 +457,8 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: sampleDocSearchResultsNoSortId, + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortId(someUuid), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -326,12 +469,14 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(mockLogger.error).toHaveBeenCalled(); expect(result).toEqual(false); }); - test('if unsuccessful iteration of searchAfterAndBulkIndex due to empty sort ids and 0 total hits', async () => { + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { const sampleParams = sampleSignalAlertParams(undefined); + const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -341,8 +486,8 @@ describe('utils', () => { }, ], }); - const result = await searchAfterAndBulkIndex({ - someResult: sampleDocSearchResultsNoSortIdNoHits, + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortIdNoHits(someUuid), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -353,11 +498,14 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { const sampleParams = sampleSignalAlertParams(10); + const oneGuid = uuid.v4(); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -368,9 +516,9 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(sampleDocSearchResultsNoSortId); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + .mockReturnValueOnce(sampleDocSearchResultsNoSortId(oneGuid)); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -381,11 +529,13 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { const sampleParams = sampleSignalAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -397,8 +547,8 @@ describe('utils', () => { ], }) .mockReturnValueOnce(sampleEmptyDocSearchResults); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -409,41 +559,13 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(true); }); - test('if logs error when iteration is unsuccessful when bulk index results in a failure', async () => { - const sampleParams = sampleSignalAlertParams(5); - mockService.callCluster - .mockReturnValueOnce({ - // first bulk insert - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - .mockReturnValueOnce(sampleDocSearchResultsWithSortId); // get some more docs - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), - signalParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleSignalId, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(result).toEqual(true); - }); test('if returns false when singleSearchAfter throws an exception', async () => { const sampleParams = sampleSignalAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ took: 100, @@ -454,9 +576,11 @@ describe('utils', () => { }, ], }) - .mockRejectedValueOnce(Error('Fake Error')); - const result = await searchAfterAndBulkIndex({ - someResult: repeatedSearchResultsWithSortId(4), + .mockImplementation(() => { + throw Error('Fake Error'); + }); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), signalParams: sampleParams, services: mockService, logger: mockLogger, @@ -467,6 +591,7 @@ describe('utils', () => { updatedBy: 'elastic', interval: '5m', enabled: true, + pageSize: 1, }); expect(result).toEqual(false); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index 25934dc9aa356..f2a3424655945 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { createHash } from 'crypto'; import { performance } from 'perf_hooks'; import { pickBy } from 'lodash/fp'; import { SignalHit, Signal } from '../../types'; @@ -59,7 +60,6 @@ export const buildRule = ({ severity: signalParams.severity, tags: signalParams.tags, type: signalParams.type, - size: signalParams.size, to: signalParams.to, enabled, filters: signalParams.filters, @@ -121,7 +121,7 @@ export const buildBulkBody = ({ return signalHit; }; -interface SingleBulkIndexParams { +interface SingleBulkCreateParams { someResult: SignalSearchResponse; signalParams: AlertTypeParams; services: AlertServices; @@ -135,8 +135,18 @@ interface SingleBulkIndexParams { enabled: boolean; } +export const generateId = ( + docIndex: string, + docId: string, + version: string, + ruleId: string +): string => + createHash('sha256') + .update(docIndex.concat(docId, version, ruleId)) + .digest('hex'); + // Bulk Index documents. -export const singleBulkIndex = async ({ +export const singleBulkCreate = async ({ someResult, signalParams, services, @@ -148,15 +158,29 @@ export const singleBulkIndex = async ({ updatedBy, interval, enabled, -}: SingleBulkIndexParams): Promise => { +}: SingleBulkCreateParams): Promise => { if (someResult.hits.hits.length === 0) { return true; } + // index documents after creating an ID based on the + // source documents' originating index, and the original + // document _id. This will allow two documents from two + // different indexes with the same ID to be + // indexed, and prevents us from creating any updates + // to the documents once inserted into the signals index, + // while preventing duplicates from being added to the + // signals index if rules are re-run over the same time + // span. Also allow for versioning. const bulkBody = someResult.hits.hits.flatMap(doc => [ { - index: { + create: { _index: signalsIndex, - _id: doc._id, + _id: generateId( + doc._index, + doc._id, + doc._version ? doc._version.toString() : '', + signalParams.ruleId ?? '' + ), }, }, buildBulkBody({ doc, signalParams, id, name, createdBy, updatedBy, interval, enabled }), @@ -171,8 +195,27 @@ export const singleBulkIndex = async ({ logger.debug(`individual bulk process time took: ${time2 - time1} milliseconds`); logger.debug(`took property says bulk took: ${firstResult.took} milliseconds`); if (firstResult.errors) { - logger.error(`[-] bulkResponse had errors: ${JSON.stringify(firstResult.errors, null, 2)}`); - return false; + // go through the response status errors and see what + // types of errors they are, count them up, and log them. + const errorCountMap = firstResult.items.reduce((acc: { [key: string]: number }, item) => { + if (item.create.error) { + const responseStatusKey = item.create.status.toString(); + acc[responseStatusKey] = acc[responseStatusKey] ? acc[responseStatusKey] + 1 : 1; + } + return acc; + }, {}); + /* + the logging output below should look like + {'409': 55} + which is read as "there were 55 counts of 409 errors returned from bulk create" + */ + logger.error( + `[-] bulkResponse had errors with response statuses:counts of...\n${JSON.stringify( + errorCountMap, + null, + 2 + )}` + ); } return true; }; @@ -182,6 +225,7 @@ interface SingleSearchAfterParams { signalParams: AlertTypeParams; services: AlertServices; logger: Logger; + pageSize: number; } // utilize search_after for paging results into bulk. @@ -190,6 +234,7 @@ export const singleSearchAfter = async ({ signalParams, services, logger, + pageSize, }: SingleSearchAfterParams): Promise => { if (searchAfterSortId == null) { throw Error('Attempted to search after with empty sort id'); @@ -200,7 +245,7 @@ export const singleSearchAfter = async ({ from: signalParams.from, to: signalParams.to, filter: signalParams.filter, - size: signalParams.size ? signalParams.size : 1000, + size: pageSize, searchAfterSortId, }); const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( @@ -214,7 +259,7 @@ export const singleSearchAfter = async ({ } }; -interface SearchAfterAndBulkIndexParams { +interface SearchAfterAndBulkCreateParams { someResult: SignalSearchResponse; signalParams: AlertTypeParams; services: AlertServices; @@ -226,10 +271,11 @@ interface SearchAfterAndBulkIndexParams { updatedBy: string; interval: string; enabled: boolean; + pageSize: number; } // search_after through documents and re-index using bulk endpoint. -export const searchAfterAndBulkIndex = async ({ +export const searchAfterAndBulkCreate = async ({ someResult, signalParams, services, @@ -241,13 +287,14 @@ export const searchAfterAndBulkIndex = async ({ updatedBy, interval, enabled, -}: SearchAfterAndBulkIndexParams): Promise => { + pageSize, +}: SearchAfterAndBulkCreateParams): Promise => { if (someResult.hits.hits.length === 0) { return true; } logger.debug('[+] starting bulk insertion'); - const firstBulkIndexSuccess = await singleBulkIndex({ + await singleBulkCreate({ someResult, signalParams, services, @@ -260,18 +307,15 @@ export const searchAfterAndBulkIndex = async ({ interval, enabled, }); - if (!firstBulkIndexSuccess) { - logger.error('First bulk index was unsuccessful'); - return false; - } - const totalHits = typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; // maxTotalHitsSize represents the total number of docs to - // query for. If maxSignals is present we will only query - // up to max signals - otherwise use the value - // from track_total_hits. - const maxTotalHitsSize = signalParams.maxSignals ? signalParams.maxSignals : totalHits; + // query for, no matter the size of each individual page of search results. + // If the total number of hits for the overall search result is greater than + // maxSignals, default to requesting a total of maxSignals, otherwise use the + // totalHits in the response from the searchAfter query. + const maxTotalHitsSize = + totalHits >= signalParams.maxSignals ? signalParams.maxSignals : totalHits; // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -295,6 +339,7 @@ export const searchAfterAndBulkIndex = async ({ signalParams, services, logger, + pageSize, // maximum number of docs to receive per search result. }); if (searchAfterResult.hits.hits.length === 0) { return true; @@ -308,7 +353,7 @@ export const searchAfterAndBulkIndex = async ({ } sortId = sortIds[0]; logger.debug('next bulk index'); - const bulkSuccess = await singleBulkIndex({ + await singleBulkCreate({ someResult: searchAfterResult, signalParams, services, @@ -322,14 +367,11 @@ export const searchAfterAndBulkIndex = async ({ enabled, }); logger.debug('finished next bulk index'); - if (!bulkSuccess) { - logger.error('[-] bulk index failed but continuing'); - } } catch (exc) { logger.error(`[-] search_after and bulk threw an error ${exc}`); return false; } } - logger.debug(`[+] completed bulk index of ${totalHits}`); + logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`); return true; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts index 7b6559561c783..fa8fd66ef2aef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts @@ -49,7 +49,6 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { risk_score: riskScore, name, severity, - size, tags, to, type, @@ -92,7 +91,6 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { riskScore, name, severity, - size, tags, to, type, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index 210ce5ca9fdce..177e7cbebc213 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -5,6 +5,7 @@ */ import Joi from 'joi'; +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; /* eslint-disable @typescript-eslint/camelcase */ const description = Joi.string(); @@ -103,7 +104,7 @@ export const createSignalsSchema = Joi.object({ }), meta, risk_score: risk_score.required(), - max_signals: max_signals.default(100), + max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), name: name.required(), severity: severity.required(), tags: tags.default([]), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts index 274c41f65a36b..1dc54f34bd1f7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts @@ -47,7 +47,6 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { risk_score: riskScore, name, severity, - size, tags, to, type, @@ -84,7 +83,6 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { riskScore, name, severity, - size, tags, to, type, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 22dd7be5fbba7..ed9e00735c704 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -37,7 +37,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -65,7 +64,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -95,7 +93,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -125,7 +122,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -153,7 +149,6 @@ describe('utils', () => { name: 'Detect Root/Admin Users', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -184,7 +179,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -215,7 +209,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -297,7 +290,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', @@ -335,7 +327,6 @@ describe('utils', () => { query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', - size: 1, updated_by: 'elastic', tags: [], to: 'now', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index e3a677741efca..7b9921b0375d8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -50,7 +50,6 @@ export const transformAlertToSignal = (signal: SignalAlertType): Partial Date: Mon, 25 Nov 2019 16:12:30 -0800 Subject: [PATCH 067/128] Chore/remove once per server (#49426) * [Reporting/np-k8] Remove several oncePerServer usages * ts fixes 1 * ts fixes 2 * more ts fixes * more ts fixes * more ts fixes * ts simplification * improve ts * remove any type for jobParams and define JobParamsSavedObject and JobParamsUrl * ts simplification * Fix ts * ts simplification * fix ts * bug fix * align with joels pr * Move get_absolute_url to not use oncePerServer * Two more removals of oncePerServer * Final once-per-server removals * AbsoluteURLFactory => AbsoluteURLFactoryOptions * Fix absolute_url util --- .../reporting/common/get_absolute_url.test.ts | 60 ++++++++----------- .../reporting/common/get_absolute_url.ts | 21 ++++--- .../common/execute_job/get_full_urls.ts | 9 ++- .../routes/lib/authorized_user_pre_routing.js | 7 +-- .../lib/reporting_feature_pre_routing.js | 7 +-- x-pack/legacy/plugins/reporting/types.d.ts | 7 +++ 6 files changed, 57 insertions(+), 54 deletions(-) diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts b/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts index 9bad3b2b08002..cb792fbd6ae03 100644 --- a/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts +++ b/x-pack/legacy/plugins/reporting/common/get_absolute_url.test.ts @@ -4,88 +4,80 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../test_helpers/create_mock_server'; import { getAbsoluteUrlFactory } from './get_absolute_url'; -test(`by default it builds url using information from server.info.protocol and the server.config`, () => { - const mockServer = createMockServer(''); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); +const defaultOptions = { + defaultBasePath: 'sbp', + protocol: 'http:', + hostname: 'localhost', + port: 5601, +}; + +test(`by default it builds urls using information from server.info.protocol and the server.config`, () => { + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana`); }); test(`uses kibanaServer.protocol if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.protocol': 'https', - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + protocol: 'https:', + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`https://localhost:5601/sbp/app/kibana`); }); test(`uses kibanaServer.hostname if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.hostname': 'something-else', - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + hostname: 'something-else', + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://something-else:5601/sbp/app/kibana`); }); test(`uses kibanaServer.port if specified`, () => { - const settings = { - 'xpack.reporting.kibanaServer.port': 8008, - }; - const mockServer = createMockServer({ settings }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ + ...defaultOptions, + port: 8008, + }); - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://localhost:8008/sbp/app/kibana`); }); test(`uses the provided hash`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const hash = '/hash'; const absoluteUrl = getAbsoluteUrl({ hash }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana#${hash}`); }); test(`uses the provided hash with queryString`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const hash = '/hash?querystring'; const absoluteUrl = getAbsoluteUrl({ hash }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana#${hash}`); }); test(`uses the provided basePath`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const absoluteUrl = getAbsoluteUrl({ basePath: '/s/marketing' }); expect(absoluteUrl).toBe(`http://localhost:5601/s/marketing/app/kibana`); }); test(`uses the path`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const path = '/app/canvas'; const absoluteUrl = getAbsoluteUrl({ path }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp${path}`); }); test(`uses the search`, () => { - const mockServer = createMockServer(''); - - const getAbsoluteUrl = getAbsoluteUrlFactory(mockServer); + const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); const search = '_t=123456789'; const absoluteUrl = getAbsoluteUrl({ search }); expect(absoluteUrl).toBe(`http://localhost:5601/sbp/app/kibana?${search}`); diff --git a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts b/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts index 1d34189abcb24..0e350cb1ec011 100644 --- a/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts +++ b/x-pack/legacy/plugins/reporting/common/get_absolute_url.ts @@ -5,24 +5,27 @@ */ import url from 'url'; -import { ServerFacade } from '../types'; - -export function getAbsoluteUrlFactory(server: ServerFacade) { - const config = server.config(); +import { AbsoluteURLFactoryOptions } from '../types'; +export const getAbsoluteUrlFactory = ({ + protocol, + hostname, + port, + defaultBasePath, +}: AbsoluteURLFactoryOptions) => { return function getAbsoluteUrl({ - basePath = config.get('server.basePath'), + basePath = defaultBasePath, hash = '', path = '/app/kibana', search = '', } = {}) { return url.format({ - protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, - hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), - port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), + protocol, + hostname, + port, pathname: basePath + path, hash, search, }); }; -} +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts index 3b82852073421..2b66c77067ed2 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/execute_job/get_full_urls.ts @@ -28,7 +28,14 @@ export async function getFullUrls({ job: JobDocPayloadPNG | JobDocPayloadPDF; server: ServerFacade; }) { - const getAbsoluteUrl = getAbsoluteUrlFactory(server); + const config = server.config(); + + const getAbsoluteUrl = getAbsoluteUrlFactory({ + defaultBasePath: config.get('server.basePath'), + protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol, + hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'), + port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'), + }); // PDF and PNG job params put in the url differently let relativeUrls: string[] = []; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js index 10ff9f477f424..59317ac46773b 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.js @@ -6,11 +6,10 @@ import boom from 'boom'; import { getUserFactory } from '../../lib/get_user'; -import { oncePerServer } from '../../lib/once_per_server'; const superuserRole = 'superuser'; -function authorizedUserPreRoutingFn(server) { +export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn(server) { const getUser = getUserFactory(server); const config = server.config(); @@ -40,6 +39,4 @@ function authorizedUserPreRoutingFn(server) { return user; }; -} - -export const authorizedUserPreRoutingFactory = oncePerServer(authorizedUserPreRoutingFn); +}; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js index ad91e5a654a4e..92973e3d0b422 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.js @@ -5,9 +5,8 @@ */ import Boom from 'boom'; -import { oncePerServer } from '../../lib/once_per_server'; -function reportingFeaturePreRoutingFn(server) { +export const reportingFeaturePreRoutingFactory = function reportingFeaturePreRoutingFn(server) { const xpackMainPlugin = server.plugins.xpack_main; const pluginId = 'reporting'; @@ -24,6 +23,4 @@ function reportingFeaturePreRoutingFn(server) { } }; }; -} - -export const reportingFeaturePreRoutingFactory = oncePerServer(reportingFeaturePreRoutingFn); +}; diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 7d05811ef4aa6..e8fb015426f51 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -326,3 +326,10 @@ export { CancellationToken } from './common/cancellation_token'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger } from './server/lib/level_logger'; + +export interface AbsoluteURLFactoryOptions { + defaultBasePath: string; + protocol: string; + hostname: string; + port: string | number; +} From ab3944c38a2603dce71f6f653197bea9b0320f4b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 25 Nov 2019 18:03:48 -0700 Subject: [PATCH 068/128] [Maps] a11y header fixes (#51511) --- x-pack/legacy/plugins/maps/public/angular/map.html | 2 ++ x-pack/legacy/plugins/maps/public/angular/map_controller.js | 1 + 2 files changed, 3 insertions(+) diff --git a/x-pack/legacy/plugins/maps/public/angular/map.html b/x-pack/legacy/plugins/maps/public/angular/map.html index 90d4ddbeb0092..2f34ffa660d6e 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map.html +++ b/x-pack/legacy/plugins/maps/public/angular/map.html @@ -1,4 +1,5 @@
    +
    +

    {{screenTitle}}

    diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 41c618d68a68e..b9354dd0a0ddd 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -66,6 +66,7 @@ const app = uiModules.get(MAP_APP_PATH, []); app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppState, globalState) => { const { filterManager } = npStart.plugins.data.query; const savedMap = $route.current.locals.map; + $scope.screenTitle = savedMap.title; let unsubscribe; let initialLayerListConfig; const $state = new AppState(); From e3a97ddd872a8d8aed5e2a2e834c57b2cfc9f459 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 25 Nov 2019 18:17:24 -0700 Subject: [PATCH 069/128] Typescriptify search source (#47644) * Initial refactor of search source * Add abort signal to search source fetch and remove cancel queued * Remove usages of Angular Promises * Remove usages of angular "sessionId" service * Remove config as dependency * Update deps on config and esShardTimeout * Remove remaining angular dependencies from SearchSource * Fix Karma tests * Separate callClient and handleResponse and add tests for each * Add tests for fetchSoon * Add back search source test and convert to jest * Create search strategy registry test * Revert empty test * Remove filter predicates from search source * Update typings and throw response errors * Fix proxy to properly return response from ES * Update jest snapshots * Remove unused translations * Don't pass search request to onRequestStart, create it afterwards * Fix search source & get search params tests * Fix issue with angular scope not firing after setting state on vis * Fix issue with angular scope not firing after setting state on vis * Typescriptify courier/search source * Fix references * Fix types * Fix removal of underscores * Fix fetching * Fix tag cloud vis * Fix setting of visConfig to not happen async * Remove unused snapshots * Remove courier and search poller * Update types * Fix issue with filters not applying * Fix issue with search embeddable time ranges * Remove deleted file again * Fix source includes * Fix searchsource constructor * Don't pass null as initial value for search source * Fix inmemorytable type * Fix issue with filters aggregation * Fix tests * Mock new platform * Fix agg filters * Fix context app tests * Fix context tests (for real) * Fix normalizeSortRequest for dateNanos * Fix issue with multiple levels of Other buckets * Adding in last commit * Review feedback * Update references to ui/courier * Fix eslint * Fix tests * Fix filter matches index for filters with partial meta * Fix type errors * Fix references * Address review feedback * Fix failing test --- src/fixtures/stubbed_search_source.js | 3 - .../data/public/search/expressions/esaggs.ts | 15 +- .../public/control/create_search_source.js | 4 +- .../saved_dashboard/saved_dashboard.d.ts | 4 +- .../angular/context/api/__tests__/anchor.js | 2 +- .../context/api/__tests__/predecessors.js | 2 +- .../context/api/__tests__/successors.js | 2 +- .../discover/angular/context/api/anchor.js | 2 +- .../discover/angular/context/api/context.ts | 5 +- .../api/utils/__tests__/sorting.test.ts | 5 +- .../api/utils/fetch_hits_in_interval.ts | 12 +- .../context/api/utils/generate_intervals.ts | 2 +- .../context/api/utils/get_es_query_sort.ts | 7 +- .../angular/context/api/utils/sorting.ts | 6 +- .../discover/embeddable/search_embeddable.ts | 12 +- .../embeddable/search_embeddable_factory.ts | 3 +- .../kibana/public/discover/kibana_services.ts | 10 +- .../kibana/public/discover/types.d.ts | 5 +- .../embeddable/visualize_embeddable.ts | 4 +- src/legacy/ui/public/agg_types/agg_config.ts | 5 +- src/legacy/ui/public/agg_types/agg_configs.ts | 3 +- .../ui/public/agg_types/buckets/terms.ts | 6 +- .../ui/public/agg_types/param_types/base.ts | 6 +- ...all_client.test.js => call_client.test.ts} | 87 ++- .../fetch/{call_client.js => call_client.ts} | 30 +- .../fetch/components/shard_failure_types.ts | 1 + src/legacy/ui/public/courier/fetch/errors.ts | 9 +- .../ui/public/courier/fetch/fetch_soon.js | 70 --- ...{fetch_soon.test.js => fetch_soon.test.ts} | 74 +-- .../ui/public/courier/fetch/fetch_soon.ts | 83 +++ ...rams.test.js => get_search_params.test.ts} | 7 +- ..._search_params.js => get_search_params.ts} | 18 +- ...sponse.test.js => handle_response.test.ts} | 24 +- ...handle_response.js => handle_response.tsx} | 30 +- .../courier/fetch/{index.js => index.ts} | 0 src/legacy/ui/public/courier/fetch/types.ts | 41 ++ .../public/courier/{index.d.ts => index.ts} | 2 + .../public/courier/search_poll/search_poll.js | 68 --- .../__tests__/normalize_sort_request.js | 124 ---- .../search_source/_normalize_sort_request.js | 88 --- ...test.js => filter_docvalue_fields.test.ts} | 7 +- ...ue_fields.js => filter_docvalue_fields.ts} | 10 +- .../public/courier/search_source/index.d.ts | 20 - .../search_source/{index.js => index.ts} | 2 +- .../ui/public/courier/search_source/mocks.ts | 19 +- .../normalize_sort_request.test.ts | 142 +++++ .../search_source/normalize_sort_request.ts | 78 +++ .../courier/search_source/search_source.d.ts | 39 -- .../courier/search_source/search_source.js | 540 ------------------ .../search_source/search_source.test.js | 193 ------- .../search_source/search_source.test.ts | 156 +++++ .../courier/search_source/search_source.ts | 410 +++++++++++++ .../ui/public/courier/search_source/types.ts | 106 ++++ ...est.js => default_search_strategy.test.ts} | 55 +- ...strategy.js => default_search_strategy.ts} | 28 +- .../search_strategy/{index.js => index.ts} | 0 ...rn.js => is_default_type_index_pattern.ts} | 4 +- ...h_strategy.js => no_op_search_strategy.ts} | 17 +- .../courier/search_strategy/search_error.d.ts | 21 - .../{search_error.js => search_error.ts} | 21 +- ...st.js => search_strategy_registry.test.ts} | 75 ++- ...egistry.js => search_strategy_registry.ts} | 24 +- .../public/courier/search_strategy/types.ts | 37 ++ .../{search_strategy/index.d.ts => types.ts} | 5 +- .../utils/courier_inspector_utils.d.ts | 52 -- ...or_utils.js => courier_inspector_utils.ts} | 45 +- .../courier/{index.js => utils/types.ts} | 21 +- .../__tests__/field_wildcard.js | 126 ---- .../field_wildcard/field_wildcard.test.ts | 86 +++ .../{field_wildcard.js => field_wildcard.ts} | 18 +- .../field_wildcard/{index.js => index.ts} | 0 src/legacy/ui/public/promises/defer.ts | 2 +- .../components/visualization_requesterror.tsx | 4 +- .../pipeline_helpers/build_pipeline.test.ts | 2 +- .../loader/pipeline_helpers/build_pipeline.ts | 9 +- .../loader/utils/query_geohash_bounds.ts | 4 +- .../es_query/filter_matches_index.test.ts | 7 + .../es_query/es_query/filter_matches_index.ts | 2 +- .../common/es_query/filters/range_filter.ts | 24 +- .../utils/highlight/highlight_request.test.ts | 21 +- .../utils/highlight/highlight_request.ts | 4 +- .../public/query/timefilter/get_time.test.ts | 6 +- .../data/public/query/timefilter/get_time.ts | 39 +- x-pack/legacy/plugins/infra/types/eui.d.ts | 1 + .../plugins/maps/public/kibana_services.js | 4 +- .../contexts/kibana/__mocks__/saved_search.ts | 3 +- .../datavisualizer/index_based/page.tsx | 2 +- .../jobs/new_job/utils/new_job_utils.ts | 10 +- .../services/new_job_capabilities_service.ts | 2 +- .../plugins/rollup/public/search/register.js | 2 +- .../public/search/rollup_search_strategy.js | 2 +- 91 files changed, 1646 insertions(+), 1742 deletions(-) rename src/legacy/ui/public/courier/fetch/{call_client.test.js => call_client.test.ts} (64%) rename src/legacy/ui/public/courier/fetch/{call_client.js => call_client.ts} (70%) delete mode 100644 src/legacy/ui/public/courier/fetch/fetch_soon.js rename src/legacy/ui/public/courier/fetch/{fetch_soon.test.js => fetch_soon.test.ts} (63%) create mode 100644 src/legacy/ui/public/courier/fetch/fetch_soon.ts rename src/legacy/ui/public/courier/fetch/{get_search_params.test.js => get_search_params.test.ts} (96%) rename src/legacy/ui/public/courier/fetch/{get_search_params.js => get_search_params.ts} (73%) rename src/legacy/ui/public/courier/fetch/{handle_response.test.js => handle_response.test.ts} (78%) rename src/legacy/ui/public/courier/fetch/{handle_response.js => handle_response.tsx} (71%) rename src/legacy/ui/public/courier/fetch/{index.js => index.ts} (100%) create mode 100644 src/legacy/ui/public/courier/fetch/types.ts rename src/legacy/ui/public/courier/{index.d.ts => index.ts} (94%) delete mode 100644 src/legacy/ui/public/courier/search_poll/search_poll.js delete mode 100644 src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js delete mode 100644 src/legacy/ui/public/courier/search_source/_normalize_sort_request.js rename src/legacy/ui/public/courier/search_source/{filter_docvalue_fields.test.js => filter_docvalue_fields.test.ts} (88%) rename src/legacy/ui/public/courier/search_source/{filter_docvalue_fields.js => filter_docvalue_fields.ts} (84%) delete mode 100644 src/legacy/ui/public/courier/search_source/index.d.ts rename src/legacy/ui/public/courier/search_source/{index.js => index.ts} (94%) create mode 100644 src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts create mode 100644 src/legacy/ui/public/courier/search_source/normalize_sort_request.ts delete mode 100644 src/legacy/ui/public/courier/search_source/search_source.d.ts delete mode 100644 src/legacy/ui/public/courier/search_source/search_source.js delete mode 100644 src/legacy/ui/public/courier/search_source/search_source.test.js create mode 100644 src/legacy/ui/public/courier/search_source/search_source.test.ts create mode 100644 src/legacy/ui/public/courier/search_source/search_source.ts create mode 100644 src/legacy/ui/public/courier/search_source/types.ts rename src/legacy/ui/public/courier/search_strategy/{default_search_strategy.test.js => default_search_strategy.test.ts} (67%) rename src/legacy/ui/public/courier/search_strategy/{default_search_strategy.js => default_search_strategy.ts} (76%) rename src/legacy/ui/public/courier/search_strategy/{index.js => index.ts} (100%) rename src/legacy/ui/public/courier/search_strategy/{is_default_type_index_pattern.js => is_default_type_index_pattern.ts} (85%) rename src/legacy/ui/public/courier/search_strategy/{no_op_search_strategy.js => no_op_search_strategy.ts} (79%) delete mode 100644 src/legacy/ui/public/courier/search_strategy/search_error.d.ts rename src/legacy/ui/public/courier/search_strategy/{search_error.js => search_error.ts} (76%) rename src/legacy/ui/public/courier/search_strategy/{search_strategy_registry.test.js => search_strategy_registry.test.ts} (58%) rename src/legacy/ui/public/courier/search_strategy/{search_strategy_registry.js => search_strategy_registry.ts} (64%) create mode 100644 src/legacy/ui/public/courier/search_strategy/types.ts rename src/legacy/ui/public/courier/{search_strategy/index.d.ts => types.ts} (84%) delete mode 100644 src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts rename src/legacy/ui/public/courier/utils/{courier_inspector_utils.js => courier_inspector_utils.ts} (78%) rename src/legacy/ui/public/courier/{index.js => utils/types.ts} (71%) delete mode 100644 src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js create mode 100644 src/legacy/ui/public/field_wildcard/field_wildcard.test.ts rename src/legacy/ui/public/field_wildcard/{field_wildcard.js => field_wildcard.ts} (70%) rename src/legacy/ui/public/field_wildcard/{index.js => index.ts} (100%) diff --git a/src/fixtures/stubbed_search_source.js b/src/fixtures/stubbed_search_source.js index 3a36b97e6757e..da741a1aa4774 100644 --- a/src/fixtures/stubbed_search_source.js +++ b/src/fixtures/stubbed_search_source.js @@ -60,9 +60,6 @@ export default function stubSearchSource(Private, $q, Promise) { onRequestStart(fn) { this._requestStartHandlers.push(fn); }, - requestIsStarting(req) { - return Promise.map(this._requestStartHandlers, fn => fn(req)); - }, requestIsStopped() {} }; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index db2a803ea1c61..7165de026920d 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -29,7 +29,12 @@ import { ExpressionFunction, KibanaDatatableColumn, } from 'src/plugins/expressions/public'; -import { SearchSource } from '../../../../../ui/public/courier/search_source'; +import { + SearchSource, + SearchSourceContract, + getRequestInspectorStats, + getResponseInspectorStats, +} from '../../../../../ui/public/courier'; // @ts-ignore import { FilterBarQueryFilterProvider, @@ -37,10 +42,6 @@ import { } from '../../../../../ui/public/filter_manager/query_filter'; import { buildTabularInspectorData } from '../../../../../ui/public/inspector/build_tabular_inspector_data'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../../../../ui/public/courier/utils/courier_inspector_utils'; import { calculateObjectHash } from '../../../../../ui/public/vis/lib/calculate_object_hash'; import { getTime } from '../../../../../ui/public/timefilter'; // @ts-ignore @@ -50,7 +51,7 @@ import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; export interface RequestHandlerParams { - searchSource: SearchSource; + searchSource: SearchSourceContract; aggs: AggConfigs; timeRange?: TimeRange; query?: Query; @@ -119,7 +120,7 @@ const handleCourierRequest = async ({ return aggs.toDsl(metricsAtAllLevels); }); - requestSearchSource.onRequestStart((paramSearchSource: SearchSource, options: any) => { + requestSearchSource.onRequestStart((paramSearchSource, options) => { return aggs.onSearchRequestStart(paramSearchSource, options); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js index 61a3d4084ab8f..2ab4131957c32 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js @@ -19,9 +19,9 @@ import { timefilter } from 'ui/timefilter'; export function createSearchSource(SearchSource, initialState, indexPattern, aggs, useTimeFilter, filters = []) { - const searchSource = new SearchSource(initialState); + const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); // Do not not inherit from rootSearchSource to avoid picking up time and globals - searchSource.setParent(false); + searchSource.setParent(undefined); searchSource.setField('filter', () => { const activeFilters = [...filters]; if (useTimeFilter) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 5b24aa13f4f77..4c417ed2954d3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SearchSource } from 'ui/courier'; import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, Query, RefreshInterval } from '../../../../../../plugins/data/public'; export interface SavedObjectDashboard extends SavedObject { @@ -34,7 +34,7 @@ export interface SavedObjectDashboard extends SavedObject { // TODO: write a migration to rid of this, it's only around for bwc. uiStateJSON?: string; lastSavedTitle: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; destroy: () => void; refreshInterval?: RefreshInterval; getQuery(): Query; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js index 46e66177b516a..4eb68c1bf50bc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/anchor.js @@ -58,7 +58,7 @@ describe('context app', function () { .then(() => { const setParentSpy = searchSourceStub.setParent; expect(setParentSpy.calledOnce).to.be(true); - expect(setParentSpy.firstCall.args[0]).to.eql(false); + expect(setParentSpy.firstCall.args[0]).to.be(undefined); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js index 2bf3da42e24e5..ea6a8c092e242 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/predecessors.js @@ -196,7 +196,7 @@ describe('context app', function () { ) .then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(false)).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); expect(setParentSpy.called).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js index b8bec40f2859c..486c8ed9b410e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/successors.js @@ -199,7 +199,7 @@ describe('context app', function () { ) .then(() => { const setParentSpy = searchSourceStub.setParent; - expect(setParentSpy.alwaysCalledWith(false)).to.be(true); + expect(setParentSpy.alwaysCalledWith(undefined)).to.be(true); expect(setParentSpy.called).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js index 62bbc6166662f..8c4cce810ca13 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/anchor.js @@ -30,7 +30,7 @@ export function fetchAnchorProvider(indexPatterns) { ) { const indexPattern = await indexPatterns.get(indexPatternId); const searchSource = new SearchSource() - .setParent(false) + .setParent(undefined) .setField('index', indexPattern) .setField('version', true) .setField('size', 1) diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts index 3314bbbf189c4..68ccf56594e72 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/context.ts @@ -17,8 +17,9 @@ * under the License. */ +import { SortDirection } from '../../../../../../../ui/public/courier'; import { IndexPatterns, IndexPattern, getServices } from '../../../kibana_services'; -import { reverseSortDir, SortDirection } from './utils/sorting'; +import { reverseSortDir } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; import { generateIntervals } from './utils/generate_intervals'; @@ -114,7 +115,7 @@ function fetchContextProvider(indexPatterns: IndexPatterns) { async function createSearchSource(indexPattern: IndexPattern, filters: esFilters.Filter[]) { return new SearchSource() - .setParent(false) + .setParent(undefined) .setField('index', indexPattern) .setField('filter', filters); } diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts index eeae2aa2c5d0a..33f4454c18d40 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/__tests__/sorting.test.ts @@ -16,7 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { reverseSortDir, SortDirection } from '../sorting'; +import { reverseSortDir } from '../sorting'; +import { SortDirection } from '../../../../../../../../../ui/public/courier'; + +jest.mock('ui/new_platform'); describe('function reverseSortDir', function() { test('reverse a given sort direction', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts index 2810e5d9d7e66..19c2ee2cdfe10 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/fetch_hits_in_interval.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { SearchSource } from '../../../../kibana_services'; +import { + EsQuerySortValue, + SortDirection, + SearchSourceContract, +} from '../../../../../../../../ui/public/courier'; import { convertTimeValueToIso } from './date_conversion'; -import { SortDirection } from './sorting'; import { EsHitRecordList } from '../context'; import { IntervalValue } from './generate_intervals'; -import { EsQuerySort } from './get_es_query_sort'; import { EsQuerySearchAfter } from './get_es_query_search_after'; interface RangeQuery { @@ -38,9 +40,9 @@ interface RangeQuery { * and filters set. */ export async function fetchHitsInInterval( - searchSource: SearchSource, + searchSource: SearchSourceContract, timeField: string, - sort: EsQuerySort, + sort: [EsQuerySortValue, EsQuerySortValue], sortDir: SortDirection, interval: IntervalValue[], searchAfter: EsQuerySearchAfter, diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts index a50764fe542b1..cb4878239ff92 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/generate_intervals.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from './sorting'; +import { SortDirection } from '../../../../../../../../ui/public/courier'; export type IntervalValue = number | null; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts index c9f9b9b939f3d..39c69112e58cb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/get_es_query_sort.ts @@ -16,11 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { SortDirection } from './sorting'; -type EsQuerySortValue = Record; - -export type EsQuerySort = [EsQuerySortValue, EsQuerySortValue]; +import { EsQuerySortValue, SortDirection } from '../../../../../../../../ui/public/courier/types'; /** * Returns `EsQuerySort` which is used to sort records in the ES query @@ -33,6 +30,6 @@ export function getEsQuerySort( timeField: string, tieBreakerField: string, sortDir: SortDirection -): EsQuerySort { +): [EsQuerySortValue, EsQuerySortValue] { return [{ [timeField]: sortDir }, { [tieBreakerField]: sortDir }]; } diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts index 4a0f531845f46..47385aecb1937 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/utils/sorting.ts @@ -17,13 +17,9 @@ * under the License. */ +import { SortDirection } from '../../../../../../../../ui/public/courier'; import { IndexPattern } from '../../../../kibana_services'; -export enum SortDirection { - asc = 'asc', - desc = 'desc', -} - /** * The list of field names that are allowed for sorting, but not included in * index pattern fields. diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index ef79cda476e51..9fee0cfc3ea00 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -22,6 +22,7 @@ import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { npStart } from 'ui/new_platform'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, TimeRange, @@ -51,7 +52,6 @@ import { getServices, IndexPattern, RequestAdapter, - SearchSource, } from '../kibana_services'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; @@ -92,7 +92,7 @@ export class SearchEmbeddable extends Embeddable private inspectorAdaptors: Adapters; private searchScope?: SearchScope; private panelTitle: string = ''; - private filtersSearchSource?: SearchSource; + private filtersSearchSource?: SearchSourceContract; private searchInstance?: JQLite; private autoRefreshFetchSubscription?: Subscription; private subscription?: Subscription; @@ -194,13 +194,11 @@ export class SearchEmbeddable extends Embeddable searchScope.inspectorAdapters = this.inspectorAdaptors; const { searchSource } = this.savedSearch; - const indexPattern = (searchScope.indexPattern = searchSource.getField('index')); + const indexPattern = (searchScope.indexPattern = searchSource.getField('index'))!; const timeRangeSearchSource = searchSource.create(); timeRangeSearchSource.setField('filter', () => { - if (!this.searchScope || !this.input.timeRange) { - return; - } + if (!this.searchScope || !this.input.timeRange) return; return getTime(indexPattern, this.input.timeRange); }); @@ -241,7 +239,7 @@ export class SearchEmbeddable extends Embeddable }; searchScope.filter = async (field, value, operator) => { - let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id); + let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id!); filters = filters.map(filter => ({ ...filter, $state: { store: esFilters.FilterStateStore.APP_STATE }, diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index 1939cc7060621..ebea646a09889 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -84,6 +84,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< const queryFilter = Private(getServices().FilterBarQueryFilterProvider); try { const savedObject = await searchLoader.get(savedObjectId); + const indexPattern = savedObject.searchSource.getField('index'); return new SearchEmbeddable( { savedSearch: savedObject, @@ -92,7 +93,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< editUrl, queryFilter, editable: getServices().capabilities.discover.save as boolean, - indexPatterns: _.compact([savedObject.searchSource.getField('index')]), + indexPatterns: indexPattern ? [indexPattern] : [], }, input, this.executeTriggerActions, diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 02b08d7fa4b61..fc5f34fab7564 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -28,7 +28,6 @@ import angular from 'angular'; // just used in embeddables and discover controll import uiRoutes from 'ui/routes'; // @ts-ignore import { uiModules } from 'ui/modules'; -import { SearchSource } from 'ui/courier'; // @ts-ignore import { StateProvider } from 'ui/state_management/state'; // @ts-ignore @@ -43,6 +42,7 @@ import { wrapInI18nContext } from 'ui/i18n'; import { docTitle } from 'ui/doc_title'; // @ts-ignore import * as docViewsRegistry from 'ui/registry/doc_views'; +import { SearchSource } from '../../../../ui/public/courier'; const services = { // new plattform @@ -87,9 +87,10 @@ export { callAfterBindingsWorkaround } from 'ui/compat'; export { getRequestInspectorStats, getResponseInspectorStats, -} from 'ui/courier/utils/courier_inspector_utils'; -// @ts-ignore -export { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; + hasSearchStategyForIndexPattern, + isDefaultTypeIndexPattern, + SearchSource, +} from '../../../../ui/public/courier'; // @ts-ignore export { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; // @ts-ignore @@ -115,7 +116,6 @@ export { unhashUrl } from 'ui/state_management/state_hashing'; // EXPORT types export { Vis } from 'ui/vis'; export { StaticIndexPattern, IndexPatterns, IndexPattern, FieldType } from 'ui/index_patterns'; -export { SearchSource } from 'ui/courier'; export { ElasticSearchHit } from 'ui/registry/doc_views_types'; export { DocViewRenderProps, DocViewRenderFn } from 'ui/registry/doc_views'; export { Adapters } from 'ui/inspector/types'; diff --git a/src/legacy/core_plugins/kibana/public/discover/types.d.ts b/src/legacy/core_plugins/kibana/public/discover/types.d.ts index 7d8740243ec02..6cdd802fa2800 100644 --- a/src/legacy/core_plugins/kibana/public/discover/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/discover/types.d.ts @@ -17,13 +17,14 @@ * under the License. */ -import { SearchSource } from './kibana_services'; +import { SearchSourceContract } from '../../../../ui/public/courier'; import { SortOrder } from './angular/doc_table/components/table_header/helpers'; +export { SortOrder } from './angular/doc_table/components/table_header/helpers'; export interface SavedSearch { readonly id: string; title: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; description?: string; columns: string[]; sort: SortOrder[]; diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 924f72594ad34..a2b46dab1ef33 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -25,12 +25,12 @@ import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/saved_object'; import { Vis } from 'ui/vis'; -import { SearchSource } from 'ui/courier'; import { queryGeohashBounds } from 'ui/visualize/loader/utils'; import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; +import { SearchSourceContract } from '../../../../../ui/public/courier'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { TimeRange, @@ -53,7 +53,7 @@ const getKeys = (o: T): Array => Object.keys(o) as Array< export interface VisSavedObject extends SavedObject { vis: Vis; description?: string; - searchSource: SearchSource; + searchSource: SearchSourceContract; title: string; uiStateJSON?: string; destroy: () => void; diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index de1a6059774e7..d4ef203721456 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -27,6 +27,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; +import { SearchSourceContract, FetchOptions } from '../courier/types'; import { AggType } from './agg_type'; import { FieldParamType } from './param_types/field'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; @@ -233,10 +234,10 @@ export class AggConfig { /** * Hook for pre-flight logic, see AggType#onSearchRequestStart * @param {Courier.SearchSource} searchSource - * @param {Courier.SearchRequest} searchRequest + * @param {Courier.FetchOptions} options * @return {Promise} */ - onSearchRequestStart(searchSource: any, options: any) { + onSearchRequestStart(searchSource: SearchSourceContract, options?: FetchOptions) { if (!this.type) { return Promise.resolve(); } diff --git a/src/legacy/ui/public/agg_types/agg_configs.ts b/src/legacy/ui/public/agg_types/agg_configs.ts index 7c0245f30a1fd..2f6951891f84d 100644 --- a/src/legacy/ui/public/agg_types/agg_configs.ts +++ b/src/legacy/ui/public/agg_types/agg_configs.ts @@ -33,6 +33,7 @@ import { Schema } from '../vis/editors/default/schemas'; import { AggConfig, AggConfigOptions } from './agg_config'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; import { IndexPattern } from '../../../core_plugins/data/public'; +import { SearchSourceContract, FetchOptions } from '../courier/types'; function removeParentAggs(obj: any) { for (const prop in obj) { @@ -301,7 +302,7 @@ export class AggConfigs { return _.find(reqAgg.getResponseAggs(), { id }); } - onSearchRequestStart(searchSource: any, options: any) { + onSearchRequestStart(searchSource: SearchSourceContract, options?: FetchOptions) { return Promise.all( // @ts-ignore this.getRequestAggs().map((agg: AggConfig) => agg.onSearchRequestStart(searchSource, options)) diff --git a/src/legacy/ui/public/agg_types/buckets/terms.ts b/src/legacy/ui/public/agg_types/buckets/terms.ts index 89e33784fb5fb..6ce0b9ce38ad3 100644 --- a/src/legacy/ui/public/agg_types/buckets/terms.ts +++ b/src/legacy/ui/public/agg_types/buckets/terms.ts @@ -19,16 +19,12 @@ import chrome from 'ui/chrome'; import { noop } from 'lodash'; -import { SearchSource } from 'ui/courier'; import { i18n } from '@kbn/i18n'; +import { SearchSource, getRequestInspectorStats, getResponseInspectorStats } from '../../courier'; import { BucketAggType, BucketAggParam } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { AggConfigOptions } from '../agg_config'; import { IBucketAggConfig } from './_bucket_agg_type'; -import { - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../courier/utils/courier_inspector_utils'; import { createFilterTerms } from './create_filter/terms'; import { wrapWithInlineComp } from './inline_comp_wrapper'; import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; diff --git a/src/legacy/ui/public/agg_types/param_types/base.ts b/src/legacy/ui/public/agg_types/param_types/base.ts index bc8ed5d485bd4..61ef73fb62e8a 100644 --- a/src/legacy/ui/public/agg_types/param_types/base.ts +++ b/src/legacy/ui/public/agg_types/param_types/base.ts @@ -20,7 +20,7 @@ import { AggParam } from '../'; import { AggConfigs } from '../agg_configs'; import { AggConfig } from '../../vis'; -import { SearchSource } from '../../courier'; +import { SearchSourceContract, FetchOptions } from '../../courier/types'; export class BaseParamType implements AggParam { name: string; @@ -55,8 +55,8 @@ export class BaseParamType implements AggParam { */ modifyAggConfigOnSearchRequestStart: ( aggConfig: AggConfig, - searchSource?: SearchSource, - options?: any + searchSource?: SearchSourceContract, + options?: FetchOptions ) => void; constructor(config: Record) { diff --git a/src/legacy/ui/public/courier/fetch/call_client.test.js b/src/legacy/ui/public/courier/fetch/call_client.test.ts similarity index 64% rename from src/legacy/ui/public/courier/fetch/call_client.test.js rename to src/legacy/ui/public/courier/fetch/call_client.test.ts index 463d6c59e479e..74c87d77dd4fd 100644 --- a/src/legacy/ui/public/courier/fetch/call_client.test.js +++ b/src/legacy/ui/public/courier/fetch/call_client.test.ts @@ -19,61 +19,64 @@ import { callClient } from './call_client'; import { handleResponse } from './handle_response'; +import { FetchHandlers, SearchRequest, SearchStrategySearchParams } from '../types'; const mockResponses = [{}, {}]; const mockAbortFns = [jest.fn(), jest.fn()]; const mockSearchFns = [ - jest.fn(({ searchRequests }) => ({ + jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[0])), - abort: mockAbortFns[0] + abort: mockAbortFns[0], })), - jest.fn(({ searchRequests }) => ({ + jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[1])), - abort: mockAbortFns[1] - })) + abort: mockAbortFns[1], + })), ]; const mockSearchStrategies = mockSearchFns.map((search, i) => ({ search, id: i })); jest.mock('./handle_response', () => ({ - handleResponse: jest.fn((request, response) => response) + handleResponse: jest.fn((request, response) => response), })); jest.mock('../search_strategy', () => ({ - getSearchStrategyForSearchRequest: request => mockSearchStrategies[request._searchStrategyId], - getSearchStrategyById: id => mockSearchStrategies[id] + getSearchStrategyForSearchRequest: (request: SearchRequest) => + mockSearchStrategies[request._searchStrategyId], + getSearchStrategyById: (id: number) => mockSearchStrategies[id], })); describe('callClient', () => { beforeEach(() => { - handleResponse.mockClear(); + (handleResponse as jest.Mock).mockClear(); mockAbortFns.forEach(fn => fn.mockClear()); mockSearchFns.forEach(fn => fn.mockClear()); }); test('Executes each search strategy with its group of matching requests', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }, { - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; - - callClient(searchRequests); + const searchRequests = [ + { _searchStrategyId: 0 }, + { _searchStrategyId: 1 }, + { _searchStrategyId: 0 }, + { _searchStrategyId: 1 }, + ]; + + callClient(searchRequests, [], {} as FetchHandlers); expect(mockSearchFns[0]).toBeCalled(); - expect(mockSearchFns[0].mock.calls[0][0].searchRequests).toEqual([searchRequests[0], searchRequests[2]]); + expect(mockSearchFns[0].mock.calls[0][0].searchRequests).toEqual([ + searchRequests[0], + searchRequests[2], + ]); expect(mockSearchFns[1]).toBeCalled(); - expect(mockSearchFns[1].mock.calls[0][0].searchRequests).toEqual([searchRequests[1], searchRequests[3]]); + expect(mockSearchFns[1].mock.calls[0][0].searchRequests).toEqual([ + searchRequests[1], + searchRequests[3], + ]); }); test('Passes the additional arguments it is given to the search strategy', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }]; - const args = { es: {}, config: {}, esShardTimeout: 0 }; + const searchRequests = [{ _searchStrategyId: 0 }]; + const args = { es: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; callClient(searchRequests, [], args); @@ -82,25 +85,17 @@ describe('callClient', () => { }); test('Returns the responses in the original order', async () => { - const searchRequests = [{ - _searchStrategyId: 1 - }, { - _searchStrategyId: 0 - }]; + const searchRequests = [{ _searchStrategyId: 1 }, { _searchStrategyId: 0 }]; - const responses = await Promise.all(callClient(searchRequests)); + const responses = await Promise.all(callClient(searchRequests, [], {} as FetchHandlers)); expect(responses).toEqual([mockResponses[1], mockResponses[0]]); }); test('Calls handleResponse with each request and response', async () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; + const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; - const responses = callClient(searchRequests); + const responses = callClient(searchRequests, [], {} as FetchHandlers); await Promise.all(responses); expect(handleResponse).toBeCalledTimes(2); @@ -109,17 +104,15 @@ describe('callClient', () => { }); test('If passed an abortSignal, calls abort on the strategy if the signal is aborted', () => { - const searchRequests = [{ - _searchStrategyId: 0 - }, { - _searchStrategyId: 1 - }]; + const searchRequests = [{ _searchStrategyId: 0 }, { _searchStrategyId: 1 }]; const abortController = new AbortController(); - const requestOptions = [{ - abortSignal: abortController.signal - }]; + const requestOptions = [ + { + abortSignal: abortController.signal, + }, + ]; - callClient(searchRequests, requestOptions); + callClient(searchRequests, requestOptions, {} as FetchHandlers); abortController.abort(); expect(mockAbortFns[0]).toBeCalled(); diff --git a/src/legacy/ui/public/courier/fetch/call_client.js b/src/legacy/ui/public/courier/fetch/call_client.ts similarity index 70% rename from src/legacy/ui/public/courier/fetch/call_client.js rename to src/legacy/ui/public/courier/fetch/call_client.ts index 971ae4c49a604..43da27f941e4e 100644 --- a/src/legacy/ui/public/courier/fetch/call_client.js +++ b/src/legacy/ui/public/courier/fetch/call_client.ts @@ -20,11 +20,20 @@ import { groupBy } from 'lodash'; import { getSearchStrategyForSearchRequest, getSearchStrategyById } from '../search_strategy'; import { handleResponse } from './handle_response'; +import { FetchOptions, FetchHandlers } from './types'; +import { SearchRequest } from '../types'; -export function callClient(searchRequests, requestsOptions = [], { es, config, esShardTimeout } = {}) { +export function callClient( + searchRequests: SearchRequest[], + requestsOptions: FetchOptions[] = [], + { es, config, esShardTimeout }: FetchHandlers +) { // Correlate the options with the request that they're associated with - const requestOptionEntries = searchRequests.map((request, i) => [request, requestsOptions[i]]); - const requestOptionsMap = new Map(requestOptionEntries); + const requestOptionEntries: Array<[ + SearchRequest, + FetchOptions + ]> = searchRequests.map((request, i) => [request, requestsOptions[i]]); + const requestOptionsMap = new Map(requestOptionEntries); // Group the requests by the strategy used to search that specific request const searchStrategyMap = groupBy(searchRequests, (request, i) => { @@ -39,15 +48,22 @@ export function callClient(searchRequests, requestsOptions = [], { es, config, e Object.keys(searchStrategyMap).forEach(searchStrategyId => { const searchStrategy = getSearchStrategyById(searchStrategyId); const requests = searchStrategyMap[searchStrategyId]; - const { searching, abort } = searchStrategy.search({ searchRequests: requests, es, config, esShardTimeout }); + + // There's no way `searchStrategy` could be undefined here because if we didn't get a matching strategy for this ID + // then an error would have been thrown above + const { searching, abort } = searchStrategy!.search({ + searchRequests: requests, + es, + config, + esShardTimeout, + }); + requests.forEach((request, i) => { const response = searching.then(results => handleResponse(request, results[i])); - const { abortSignal } = requestOptionsMap.get(request) || {}; + const { abortSignal = null } = requestOptionsMap.get(request) || {}; if (abortSignal) abortSignal.addEventListener('abort', abort); requestResponseMap.set(request, response); }); }, []); return searchRequests.map(request => requestResponseMap.get(request)); } - - diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts index de32b9d7b3087..22fc20233cc87 100644 --- a/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts +++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts @@ -24,6 +24,7 @@ export interface Request { sort: unknown; stored_fields: string[]; } + export interface ResponseWithShardFailure { _shards: { failed: number; diff --git a/src/legacy/ui/public/courier/fetch/errors.ts b/src/legacy/ui/public/courier/fetch/errors.ts index aba554a795258..a2ac013915b4b 100644 --- a/src/legacy/ui/public/courier/fetch/errors.ts +++ b/src/legacy/ui/public/courier/fetch/errors.ts @@ -17,17 +17,18 @@ * under the License. */ +import { SearchError } from '../../courier'; import { KbnError } from '../../../../../plugins/kibana_utils/public'; +import { SearchResponse } from '../types'; /** * Request Failure - When an entire multi request fails * @param {Error} err - the Error that came back * @param {Object} resp - optional HTTP response */ export class RequestFailure extends KbnError { - public resp: any; - constructor(err: any, resp?: any) { - err = err || false; - super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err.message)}`); + public resp: SearchResponse; + constructor(err: SearchError | null = null, resp?: SearchResponse) { + super(`Request to Elasticsearch failed: ${JSON.stringify(resp || err?.message)}`); this.resp = resp; } diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.js b/src/legacy/ui/public/courier/fetch/fetch_soon.js deleted file mode 100644 index ef02beddcb59a..0000000000000 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { callClient } from './call_client'; - -/** - * This function introduces a slight delay in the request process to allow multiple requests to queue - * up (e.g. when a dashboard is loading). - */ -export async function fetchSoon(request, options, { es, config, esShardTimeout }) { - const delay = config.get('courier:batchSearches') ? 50 : 0; - return delayedFetch(request, options, { es, config, esShardTimeout }, delay); -} - -/** - * Delays executing a function for a given amount of time, and returns a promise that resolves - * with the result. - * @param fn The function to invoke - * @param ms The number of milliseconds to wait - * @return Promise A promise that resolves with the result of executing the function - */ -function delay(fn, ms) { - return new Promise(resolve => { - setTimeout(() => resolve(fn()), ms); - }); -} - -// The current batch/queue of requests to fetch -let requestsToFetch = []; -let requestOptions = []; - -// The in-progress fetch (if there is one) -let fetchInProgress = null; - -/** - * Delay fetching for a given amount of time, while batching up the requests to be fetched. - * Returns a promise that resolves with the response for the given request. - * @param request The request to fetch - * @param ms The number of milliseconds to wait (and batch requests) - * @return Promise The response for the given request - */ -async function delayedFetch(request, options, { es, config, esShardTimeout }, ms) { - const i = requestsToFetch.length; - requestsToFetch = [...requestsToFetch, request]; - requestOptions = [...requestOptions, options]; - const responses = await (fetchInProgress = fetchInProgress || delay(() => { - const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); - requestsToFetch = []; - requestOptions = []; - fetchInProgress = null; - return response; - }, ms)); - return responses[i]; -} diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.test.js b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts similarity index 63% rename from src/legacy/ui/public/courier/fetch/fetch_soon.test.js rename to src/legacy/ui/public/courier/fetch/fetch_soon.test.ts index 824a4ab7e12e3..e753c526b748d 100644 --- a/src/legacy/ui/public/courier/fetch/fetch_soon.test.js +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.test.ts @@ -19,47 +19,53 @@ import { fetchSoon } from './fetch_soon'; import { callClient } from './call_client'; - -function getMockConfig(config) { - const entries = Object.entries(config); - return new Map(entries); +import { UiSettingsClientContract } from '../../../../../core/public'; +import { FetchHandlers, FetchOptions } from './types'; +import { SearchRequest, SearchResponse } from '../types'; + +function getConfigStub(config: any = {}) { + return { + get: key => config[key], + } as UiSettingsClientContract; } -const mockResponses = { - 'foo': {}, - 'bar': {}, - 'baz': {}, +const mockResponses: Record = { + foo: {}, + bar: {}, + baz: {}, }; jest.useFakeTimers(); jest.mock('./call_client', () => ({ - callClient: jest.fn(requests => { + callClient: jest.fn((requests: SearchRequest[]) => { // Allow a request object to specify which mockResponse it wants to receive (_mockResponseId) // in addition to how long to simulate waiting before returning a response (_waitMs) const responses = requests.map(request => { - const waitMs = requests.reduce((total, request) => request._waitMs || 0, 0); + const waitMs = requests.reduce((total, { _waitMs }) => total + _waitMs || 0, 0); return new Promise(resolve => { - resolve(mockResponses[request._mockResponseId]); - }, waitMs); + setTimeout(() => { + resolve(mockResponses[request._mockResponseId]); + }, waitMs); + }); }); return Promise.resolve(responses); - }) + }), })); describe('fetchSoon', () => { beforeEach(() => { - callClient.mockClear(); + (callClient as jest.Mock).mockClear(); }); test('should delay by 0ms if config is set to not batch searches', () => { - const config = getMockConfig({ - 'courier:batchSearches': false + const config = getConfigStub({ + 'courier:batchSearches': false, }); const request = {}; const options = {}; - fetchSoon(request, options, { config }); + fetchSoon(request, options, { config } as FetchHandlers); expect(callClient).not.toBeCalled(); jest.advanceTimersByTime(0); @@ -67,13 +73,13 @@ describe('fetchSoon', () => { }); test('should delay by 50ms if config is set to batch searches', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const request = {}; const options = {}; - fetchSoon(request, options, { config }); + fetchSoon(request, options, { config } as FetchHandlers); expect(callClient).not.toBeCalled(); jest.advanceTimersByTime(0); @@ -83,30 +89,30 @@ describe('fetchSoon', () => { }); test('should send a batch of requests to callClient', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const requests = [{ foo: 1 }, { foo: 2 }]; const options = [{ bar: 1 }, { bar: 2 }]; requests.forEach((request, i) => { - fetchSoon(request, options[i], { config }); + fetchSoon(request, options[i] as FetchOptions, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); expect(callClient).toBeCalledTimes(1); - expect(callClient.mock.calls[0][0]).toEqual(requests); - expect(callClient.mock.calls[0][1]).toEqual(options); + expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(requests); + expect((callClient as jest.Mock).mock.calls[0][1]).toEqual(options); }); test('should return the response to the corresponding call for multiple batched requests', async () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const requests = [{ _mockResponseId: 'foo' }, { _mockResponseId: 'bar' }]; const promises = requests.map(request => { - return fetchSoon(request, {}, { config }); + return fetchSoon(request, {}, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); const results = await Promise.all(promises); @@ -115,26 +121,26 @@ describe('fetchSoon', () => { }); test('should wait for the previous batch to start before starting a new batch', () => { - const config = getMockConfig({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); const firstBatch = [{ foo: 1 }, { foo: 2 }]; const secondBatch = [{ bar: 1 }, { bar: 2 }]; firstBatch.forEach(request => { - fetchSoon(request, {}, { config }); + fetchSoon(request, {}, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); secondBatch.forEach(request => { - fetchSoon(request, {}, { config }); + fetchSoon(request, {}, { config } as FetchHandlers); }); expect(callClient).toBeCalledTimes(1); - expect(callClient.mock.calls[0][0]).toEqual(firstBatch); + expect((callClient as jest.Mock).mock.calls[0][0]).toEqual(firstBatch); jest.advanceTimersByTime(50); expect(callClient).toBeCalledTimes(2); - expect(callClient.mock.calls[1][0]).toEqual(secondBatch); + expect((callClient as jest.Mock).mock.calls[1][0]).toEqual(secondBatch); }); }); diff --git a/src/legacy/ui/public/courier/fetch/fetch_soon.ts b/src/legacy/ui/public/courier/fetch/fetch_soon.ts new file mode 100644 index 0000000000000..75de85e02a1a2 --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/fetch_soon.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { callClient } from './call_client'; +import { FetchHandlers, FetchOptions } from './types'; +import { SearchRequest, SearchResponse } from '../types'; + +/** + * This function introduces a slight delay in the request process to allow multiple requests to queue + * up (e.g. when a dashboard is loading). + */ +export async function fetchSoon( + request: SearchRequest, + options: FetchOptions, + { es, config, esShardTimeout }: FetchHandlers +) { + const msToDelay = config.get('courier:batchSearches') ? 50 : 0; + return delayedFetch(request, options, { es, config, esShardTimeout }, msToDelay); +} + +/** + * Delays executing a function for a given amount of time, and returns a promise that resolves + * with the result. + * @param fn The function to invoke + * @param ms The number of milliseconds to wait + * @return Promise A promise that resolves with the result of executing the function + */ +function delay(fn: Function, ms: number) { + return new Promise(resolve => { + setTimeout(() => resolve(fn()), ms); + }); +} + +// The current batch/queue of requests to fetch +let requestsToFetch: SearchRequest[] = []; +let requestOptions: FetchOptions[] = []; + +// The in-progress fetch (if there is one) +let fetchInProgress: Promise | null = null; + +/** + * Delay fetching for a given amount of time, while batching up the requests to be fetched. + * Returns a promise that resolves with the response for the given request. + * @param request The request to fetch + * @param ms The number of milliseconds to wait (and batch requests) + * @return Promise The response for the given request + */ +async function delayedFetch( + request: SearchRequest, + options: FetchOptions, + { es, config, esShardTimeout }: FetchHandlers, + ms: number +) { + const i = requestsToFetch.length; + requestsToFetch = [...requestsToFetch, request]; + requestOptions = [...requestOptions, options]; + const responses = await (fetchInProgress = + fetchInProgress || + delay(() => { + const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); + requestsToFetch = []; + requestOptions = []; + fetchInProgress = null; + return response; + }, ms)); + return responses[i]; +} diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.test.js b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts similarity index 96% rename from src/legacy/ui/public/courier/fetch/get_search_params.test.js rename to src/legacy/ui/public/courier/fetch/get_search_params.test.ts index 380d1da963ddf..d6f3d33099599 100644 --- a/src/legacy/ui/public/courier/fetch/get_search_params.test.js +++ b/src/legacy/ui/public/courier/fetch/get_search_params.test.ts @@ -18,11 +18,12 @@ */ import { getMSearchParams, getSearchParams } from './get_search_params'; +import { UiSettingsClientContract } from '../../../../../core/public'; -function getConfigStub(config = {}) { +function getConfigStub(config: any = {}) { return { - get: key => config[key] - }; + get: key => config[key], + } as UiSettingsClientContract; } describe('getMSearchParams', () => { diff --git a/src/legacy/ui/public/courier/fetch/get_search_params.js b/src/legacy/ui/public/courier/fetch/get_search_params.ts similarity index 73% rename from src/legacy/ui/public/courier/fetch/get_search_params.js rename to src/legacy/ui/public/courier/fetch/get_search_params.ts index dd55201ba5540..6b8da07ca93d4 100644 --- a/src/legacy/ui/public/courier/fetch/get_search_params.js +++ b/src/legacy/ui/public/courier/fetch/get_search_params.ts @@ -17,9 +17,11 @@ * under the License. */ +import { UiSettingsClientContract } from '../../../../../core/public'; + const sessionId = Date.now(); -export function getMSearchParams(config) { +export function getMSearchParams(config: UiSettingsClientContract) { return { rest_total_hits_as_int: true, ignore_throttled: getIgnoreThrottled(config), @@ -27,7 +29,7 @@ export function getMSearchParams(config) { }; } -export function getSearchParams(config, esShardTimeout) { +export function getSearchParams(config: UiSettingsClientContract, esShardTimeout: number = 0) { return { rest_total_hits_as_int: true, ignore_unavailable: true, @@ -38,21 +40,23 @@ export function getSearchParams(config, esShardTimeout) { }; } -export function getIgnoreThrottled(config) { +export function getIgnoreThrottled(config: UiSettingsClientContract) { return !config.get('search:includeFrozen'); } -export function getMaxConcurrentShardRequests(config) { +export function getMaxConcurrentShardRequests(config: UiSettingsClientContract) { const maxConcurrentShardRequests = config.get('courier:maxConcurrentShardRequests'); return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; } -export function getPreference(config) { +export function getPreference(config: UiSettingsClientContract) { const setRequestPreference = config.get('courier:setRequestPreference'); if (setRequestPreference === 'sessionId') return sessionId; - return setRequestPreference === 'custom' ? config.get('courier:customRequestPreference') : undefined; + return setRequestPreference === 'custom' + ? config.get('courier:customRequestPreference') + : undefined; } -export function getTimeout(esShardTimeout) { +export function getTimeout(esShardTimeout: number) { return esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined; } diff --git a/src/legacy/ui/public/courier/fetch/handle_response.test.js b/src/legacy/ui/public/courier/fetch/handle_response.test.ts similarity index 78% rename from src/legacy/ui/public/courier/fetch/handle_response.test.js rename to src/legacy/ui/public/courier/fetch/handle_response.test.ts index 0836832e6c05a..0163aca777161 100644 --- a/src/legacy/ui/public/courier/fetch/handle_response.test.js +++ b/src/legacy/ui/public/courier/fetch/handle_response.test.ts @@ -23,46 +23,50 @@ import { toastNotifications } from '../../notify/toasts'; jest.mock('../../notify/toasts', () => { return { toastNotifications: { - addWarning: jest.fn() - } + addWarning: jest.fn(), + }, }; }); jest.mock('@kbn/i18n', () => { return { i18n: { - translate: (id, { defaultMessage }) => defaultMessage - } + translate: (id: string, { defaultMessage }: { defaultMessage: string }) => defaultMessage, + }, }; }); describe('handleResponse', () => { beforeEach(() => { - toastNotifications.addWarning.mockReset(); + (toastNotifications.addWarning as jest.Mock).mockReset(); }); test('should notify if timed out', () => { const request = { body: {} }; const response = { - timed_out: true + timed_out: true, }; const result = handleResponse(request, response); expect(result).toBe(response); expect(toastNotifications.addWarning).toBeCalled(); - expect(toastNotifications.addWarning.mock.calls[0][0].title).toMatch('request timed out'); + expect((toastNotifications.addWarning as jest.Mock).mock.calls[0][0].title).toMatch( + 'request timed out' + ); }); test('should notify if shards failed', () => { const request = { body: {} }; const response = { _shards: { - failed: true - } + failed: true, + }, }; const result = handleResponse(request, response); expect(result).toBe(response); expect(toastNotifications.addWarning).toBeCalled(); - expect(toastNotifications.addWarning.mock.calls[0][0].title).toMatch('shards failed'); + expect((toastNotifications.addWarning as jest.Mock).mock.calls[0][0].title).toMatch( + 'shards failed' + ); }); test('returns the response', () => { diff --git a/src/legacy/ui/public/courier/fetch/handle_response.js b/src/legacy/ui/public/courier/fetch/handle_response.tsx similarity index 71% rename from src/legacy/ui/public/courier/fetch/handle_response.js rename to src/legacy/ui/public/courier/fetch/handle_response.tsx index fb2797369d78f..d7f2263268f8c 100644 --- a/src/legacy/ui/public/courier/fetch/handle_response.js +++ b/src/legacy/ui/public/courier/fetch/handle_response.tsx @@ -17,14 +17,16 @@ * under the License. */ - import React from 'react'; -import { toastNotifications } from '../../notify/toasts'; import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; +import { toastNotifications } from '../../notify/toasts'; import { ShardFailureOpenModalButton } from './components/shard_failure_open_modal_button'; +import { Request, ResponseWithShardFailure } from './components/shard_failure_types'; +import { SearchRequest, SearchResponse } from '../types'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; -export function handleResponse(request, response) { +export function handleResponse(request: SearchRequest, response: SearchResponse) { if (response.timed_out) { toastNotifications.addWarning({ title: i18n.translate('common.ui.courier.fetch.requestTimedOutNotificationMessage', { @@ -41,26 +43,26 @@ export function handleResponse(request, response) { shardsTotal: response._shards.total, }, }); - const description = i18n.translate('common.ui.courier.fetch.shardsFailedNotificationDescription', { - defaultMessage: 'The data you are seeing might be incomplete or wrong.', - }); + const description = i18n.translate( + 'common.ui.courier.fetch.shardsFailedNotificationDescription', + { + defaultMessage: 'The data you are seeing might be incomplete or wrong.', + } + ); - const text = ( + const text = toMountPoint( <> {description} - + ); - toastNotifications.addWarning({ - title, - text, - }); + toastNotifications.addWarning({ title, text }); } return response; diff --git a/src/legacy/ui/public/courier/fetch/index.js b/src/legacy/ui/public/courier/fetch/index.ts similarity index 100% rename from src/legacy/ui/public/courier/fetch/index.js rename to src/legacy/ui/public/courier/fetch/index.ts diff --git a/src/legacy/ui/public/courier/fetch/types.ts b/src/legacy/ui/public/courier/fetch/types.ts new file mode 100644 index 0000000000000..e341e1ab35c5c --- /dev/null +++ b/src/legacy/ui/public/courier/fetch/types.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UiSettingsClientContract } from '../../../../../core/public'; +import { SearchRequest, SearchResponse } from '../types'; + +export interface ApiCaller { + search: (searchRequest: SearchRequest) => ApiCallerResponse; + msearch: (searchRequest: SearchRequest) => ApiCallerResponse; +} + +export interface ApiCallerResponse extends Promise { + abort: () => void; +} + +export interface FetchOptions { + abortSignal?: AbortSignal; + searchStrategyId?: string; +} + +export interface FetchHandlers { + es: ApiCaller; + config: UiSettingsClientContract; + esShardTimeout: number; +} diff --git a/src/legacy/ui/public/courier/index.d.ts b/src/legacy/ui/public/courier/index.ts similarity index 94% rename from src/legacy/ui/public/courier/index.d.ts rename to src/legacy/ui/public/courier/index.ts index 93556c2666c9a..3c16926d2aba7 100644 --- a/src/legacy/ui/public/courier/index.d.ts +++ b/src/legacy/ui/public/courier/index.ts @@ -17,6 +17,8 @@ * under the License. */ +export * from './fetch'; export * from './search_source'; export * from './search_strategy'; export * from './utils/courier_inspector_utils'; +export * from './types'; diff --git a/src/legacy/ui/public/courier/search_poll/search_poll.js b/src/legacy/ui/public/courier/search_poll/search_poll.js deleted file mode 100644 index f00c2a32e0ec6..0000000000000 --- a/src/legacy/ui/public/courier/search_poll/search_poll.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -import { timefilter } from 'ui/timefilter'; - -export class SearchPoll { - constructor() { - this._isPolling = false; - this._intervalInMs = undefined; - this._timerId = null; - } - - setIntervalInMs = intervalInMs => { - this._intervalInMs = _.parseInt(intervalInMs); - }; - - resume = () => { - this._isPolling = true; - this.resetTimer(); - }; - - pause = () => { - this._isPolling = false; - this.clearTimer(); - }; - - resetTimer = () => { - // Cancel the pending search and schedule a new one. - this.clearTimer(); - - if (this._isPolling) { - this._timerId = setTimeout(this._search, this._intervalInMs); - } - }; - - clearTimer = () => { - // Cancel the pending search, if there is one. - if (this._timerId) { - clearTimeout(this._timerId); - this._timerId = null; - } - }; - - _search = () => { - // Schedule another search. - this.resetTimer(); - - timefilter.notifyShouldFetch(); - }; -} diff --git a/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js b/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js deleted file mode 100644 index 279e389dec114..0000000000000 --- a/src/legacy/ui/public/courier/search_source/__tests__/normalize_sort_request.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import '../../../private'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import { normalizeSortRequest } from '../_normalize_sort_request'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import _ from 'lodash'; - -describe('SearchSource#normalizeSortRequest', function () { - let indexPattern; - let normalizedSort; - const defaultSortOptions = { unmapped_type: 'boolean' }; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - normalizedSort = [{ - someField: { - order: 'desc', - unmapped_type: 'boolean' - } - }]; - })); - - it('should return an array', function () { - const sortable = { someField: 'desc' }; - const result = normalizeSortRequest(sortable, indexPattern, defaultSortOptions); - expect(result).to.be.an(Array); - expect(result).to.eql(normalizedSort); - // ensure object passed in is not mutated - expect(result[0]).to.not.be.equal(sortable); - expect(sortable).to.eql({ someField: 'desc' }); - }); - - it('should make plain string sort into the more verbose format', function () { - const result = normalizeSortRequest([{ someField: 'desc' }], indexPattern, defaultSortOptions); - expect(result).to.eql(normalizedSort); - }); - - it('should append default sort options', function () { - const sortState = [{ - someField: { - order: 'desc', - unmapped_type: 'boolean' - } - }]; - const result = normalizeSortRequest(sortState, indexPattern, defaultSortOptions); - expect(result).to.eql(normalizedSort); - }); - - it('should enable script based sorting', function () { - const fieldName = 'script string'; - const direction = 'desc'; - const indexField = indexPattern.fields.getByName(fieldName); - - const sortState = {}; - sortState[fieldName] = direction; - normalizedSort = { - _script: { - script: { - source: indexField.script, - lang: indexField.lang - }, - type: indexField.type, - order: direction - } - }; - - let result = normalizeSortRequest(sortState, indexPattern, defaultSortOptions); - expect(result).to.eql([normalizedSort]); - - sortState[fieldName] = { order: direction }; - result = normalizeSortRequest([sortState], indexPattern, defaultSortOptions); - expect(result).to.eql([normalizedSort]); - }); - - it('should use script based sorting only on sortable types', function () { - const fieldName = 'script murmur3'; - const direction = 'asc'; - - const sortState = {}; - sortState[fieldName] = direction; - normalizedSort = {}; - normalizedSort[fieldName] = { - order: direction, - unmapped_type: 'boolean' - }; - const result = normalizeSortRequest([sortState], indexPattern, defaultSortOptions); - - expect(result).to.eql([normalizedSort]); - }); - - it('should remove unmapped_type parameter from _score sorting', function () { - const sortable = { _score: 'desc' }; - const expected = [{ - _score: { - order: 'desc' - } - }]; - - const result = normalizeSortRequest(sortable, indexPattern, defaultSortOptions); - expect(_.isEqual(result, expected)).to.be.ok(); - - }); -}); diff --git a/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js b/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js deleted file mode 100644 index 3e5d7a1374115..0000000000000 --- a/src/legacy/ui/public/courier/search_source/_normalize_sort_request.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -/** - * Decorate queries with default parameters - * @param {query} query object - * @returns {object} - */ -export function normalizeSortRequest(sortObject, indexPattern, defaultSortOptions) { - // [].concat({}) -> [{}], [].concat([{}]) -> [{}] - return [].concat(sortObject).map(function (sortable) { - return normalize(sortable, indexPattern, defaultSortOptions); - }); -} - -/* - Normalize the sort description to the more verbose format: - { someField: "desc" } into { someField: { "order": "desc"}} - */ -function normalize(sortable, indexPattern, defaultSortOptions) { - const normalized = {}; - let sortField = _.keys(sortable)[0]; - let sortValue = sortable[sortField]; - const indexField = indexPattern.fields.getByName(sortField); - - if (indexField && indexField.scripted && indexField.sortable) { - let direction; - if (_.isString(sortValue)) direction = sortValue; - if (_.isObject(sortValue) && sortValue.order) direction = sortValue.order; - - sortField = '_script'; - sortValue = { - script: { - source: indexField.script, - lang: indexField.lang - }, - type: castSortType(indexField.type), - order: direction - }; - } else { - if (_.isString(sortValue)) { - sortValue = { order: sortValue }; - } - sortValue = _.defaults({}, sortValue, defaultSortOptions); - - if (sortField === '_score') { - delete sortValue.unmapped_type; - } - } - - normalized[sortField] = sortValue; - return normalized; -} - -// The ES API only supports sort scripts of type 'number' and 'string' -function castSortType(type) { - const typeCastings = { - number: 'number', - string: 'string', - date: 'number', - boolean: 'string' - }; - - const castedType = typeCastings[type]; - if (!castedType) { - throw new Error(`Unsupported script sort type: ${type}`); - } - - return castedType; -} diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts similarity index 88% rename from src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js rename to src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts index b220361e33b3b..522117fe22804 100644 --- a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.js +++ b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.test.ts @@ -23,11 +23,8 @@ test('Should exclude docvalue_fields that are not contained in fields', () => { const docvalueFields = [ 'my_ip_field', { field: 'my_keyword_field' }, - { field: 'my_date_field', 'format': 'epoch_millis' } + { field: 'my_date_field', format: 'epoch_millis' }, ]; const out = filterDocvalueFields(docvalueFields, ['my_ip_field', 'my_keyword_field']); - expect(out).toEqual([ - 'my_ip_field', - { field: 'my_keyword_field' }, - ]); + expect(out).toEqual(['my_ip_field', { field: 'my_keyword_field' }]); }); diff --git a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts similarity index 84% rename from src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js rename to src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts index cd726709b4b5c..917d26f0decd1 100644 --- a/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.js +++ b/src/legacy/ui/public/courier/search_source/filter_docvalue_fields.ts @@ -17,7 +17,15 @@ * under the License. */ -export function filterDocvalueFields(docvalueFields, fields) { +interface DocvalueField { + field: string; + [key: string]: unknown; +} + +export function filterDocvalueFields( + docvalueFields: Array, + fields: string[] +) { return docvalueFields.filter(docValue => { const docvalueFieldName = typeof docValue === 'string' ? docValue : docValue.field; return fields.includes(docvalueFieldName); diff --git a/src/legacy/ui/public/courier/search_source/index.d.ts b/src/legacy/ui/public/courier/search_source/index.d.ts deleted file mode 100644 index dcae7b3d2ff05..0000000000000 --- a/src/legacy/ui/public/courier/search_source/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { SearchSource } from './search_source'; diff --git a/src/legacy/ui/public/courier/search_source/index.js b/src/legacy/ui/public/courier/search_source/index.ts similarity index 94% rename from src/legacy/ui/public/courier/search_source/index.js rename to src/legacy/ui/public/courier/search_source/index.ts index dcae7b3d2ff05..72170adc2b129 100644 --- a/src/legacy/ui/public/courier/search_source/index.js +++ b/src/legacy/ui/public/courier/search_source/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { SearchSource } from './search_source'; +export * from './search_source'; diff --git a/src/legacy/ui/public/courier/search_source/mocks.ts b/src/legacy/ui/public/courier/search_source/mocks.ts index bf546c1b9e7c2..2b83f379b4f09 100644 --- a/src/legacy/ui/public/courier/search_source/mocks.ts +++ b/src/legacy/ui/public/courier/search_source/mocks.ts @@ -36,21 +36,22 @@ * under the License. */ -export const searchSourceMock = { +import { SearchSourceContract } from './search_source'; + +export const searchSourceMock: MockedKeys = { setPreferredSearchStrategyId: jest.fn(), - getPreferredSearchStrategyId: jest.fn(), - setFields: jest.fn(), - setField: jest.fn(), + setFields: jest.fn().mockReturnThis(), + setField: jest.fn().mockReturnThis(), getId: jest.fn(), getFields: jest.fn(), getField: jest.fn(), getOwnField: jest.fn(), - create: jest.fn(), - createCopy: jest.fn(), - createChild: jest.fn(), + create: jest.fn().mockReturnThis(), + createCopy: jest.fn().mockReturnThis(), + createChild: jest.fn().mockReturnThis(), setParent: jest.fn(), - getParent: jest.fn(), - fetch: jest.fn(), + getParent: jest.fn().mockReturnThis(), + fetch: jest.fn().mockResolvedValue({}), onRequestStart: jest.fn(), getSearchRequestBody: jest.fn(), destroy: jest.fn(), diff --git a/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts b/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts new file mode 100644 index 0000000000000..d27b01eb5cf7c --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/normalize_sort_request.test.ts @@ -0,0 +1,142 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { normalizeSortRequest } from './normalize_sort_request'; +import { SortDirection } from './types'; +import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns'; + +jest.mock('ui/new_platform'); + +describe('SearchSource#normalizeSortRequest', function() { + const scriptedField = { + name: 'script string', + type: 'number', + scripted: true, + sortable: true, + script: 'foo', + lang: 'painless', + }; + const murmurScriptedField = { + ...scriptedField, + sortable: false, + name: 'murmur script', + type: 'murmur3', + }; + const indexPattern = { + fields: [scriptedField, murmurScriptedField], + } as IndexPattern; + + it('should return an array', function() { + const sortable = { someField: SortDirection.desc }; + const result = normalizeSortRequest(sortable, indexPattern); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + }, + }, + ]); + // ensure object passed in is not mutated + expect(result[0]).not.toBe(sortable); + expect(sortable).toEqual({ someField: SortDirection.desc }); + }); + + it('should make plain string sort into the more verbose format', function() { + const result = normalizeSortRequest([{ someField: SortDirection.desc }], indexPattern); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + }, + }, + ]); + }); + + it('should append default sort options', function() { + const defaultSortOptions = { + unmapped_type: 'boolean', + }; + const result = normalizeSortRequest( + [{ someField: SortDirection.desc }], + indexPattern, + defaultSortOptions + ); + expect(result).toEqual([ + { + someField: { + order: SortDirection.desc, + ...defaultSortOptions, + }, + }, + ]); + }); + + it('should enable script based sorting', function() { + const result = normalizeSortRequest( + { + [scriptedField.name]: SortDirection.desc, + }, + indexPattern + ); + expect(result).toEqual([ + { + _script: { + script: { + source: scriptedField.script, + lang: scriptedField.lang, + }, + type: scriptedField.type, + order: SortDirection.desc, + }, + }, + ]); + }); + + it('should use script based sorting only on sortable types', function() { + const result = normalizeSortRequest( + [ + { + [murmurScriptedField.name]: SortDirection.asc, + }, + ], + indexPattern + ); + + expect(result).toEqual([ + { + [murmurScriptedField.name]: { + order: SortDirection.asc, + }, + }, + ]); + }); + + it('should remove unmapped_type parameter from _score sorting', function() { + const result = normalizeSortRequest({ _score: SortDirection.desc }, indexPattern, { + unmapped_type: 'boolean', + }); + expect(result).toEqual([ + { + _score: { + order: SortDirection.desc, + }, + }, + ]); + }); +}); diff --git a/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts b/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts new file mode 100644 index 0000000000000..0f8fc8076caa0 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/normalize_sort_request.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../../core_plugins/data/public'; +import { EsQuerySortValue, SortOptions } from './types'; + +export function normalizeSortRequest( + sortObject: EsQuerySortValue | EsQuerySortValue[], + indexPattern: IndexPattern | string | undefined, + defaultSortOptions: SortOptions = {} +) { + const sortArray: EsQuerySortValue[] = Array.isArray(sortObject) ? sortObject : [sortObject]; + return sortArray.map(function(sortable) { + return normalize(sortable, indexPattern, defaultSortOptions); + }); +} + +/** + * Normalize the sort description to the more verbose format (e.g. { someField: "desc" } into + * { someField: { "order": "desc"}}), and convert sorts on scripted fields into the proper script + * for Elasticsearch. Mix in the default options according to the advanced settings. + */ +function normalize( + sortable: EsQuerySortValue, + indexPattern: IndexPattern | string | undefined, + defaultSortOptions: any +) { + const [[sortField, sortOrder]] = Object.entries(sortable); + const order = typeof sortOrder === 'object' ? sortOrder : { order: sortOrder }; + + if (indexPattern && typeof indexPattern !== 'string') { + const indexField = indexPattern.fields.find(({ name }) => name === sortField); + if (indexField && indexField.scripted && indexField.sortable) { + return { + _script: { + script: { + source: indexField.script, + lang: indexField.lang, + }, + type: castSortType(indexField.type), + ...order, + }, + }; + } + } + + // Don't include unmapped_type for _score field + const { unmapped_type, ...otherSortOptions } = defaultSortOptions; + return { + [sortField]: { ...order, ...(sortField === '_score' ? otherSortOptions : defaultSortOptions) }, + }; +} + +// The ES API only supports sort scripts of type 'number' and 'string' +function castSortType(type: string) { + if (['number', 'string'].includes(type)) { + return 'number'; + } else if (['string', 'boolean'].includes(type)) { + return 'string'; + } + throw new Error(`Unsupported script sort type: ${type}`); +} diff --git a/src/legacy/ui/public/courier/search_source/search_source.d.ts b/src/legacy/ui/public/courier/search_source/search_source.d.ts deleted file mode 100644 index 674e7ace0594c..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export declare class SearchSource { - setPreferredSearchStrategyId: (searchStrategyId: string) => void; - getPreferredSearchStrategyId: () => string; - setFields: (newFields: any) => SearchSource; - setField: (field: string, value: any) => SearchSource; - getId: () => string; - getFields: () => any; - getField: (field: string) => any; - getOwnField: () => any; - create: () => SearchSource; - createCopy: () => SearchSource; - createChild: (options?: any) => SearchSource; - setParent: (parent: SearchSource | boolean) => SearchSource; - getParent: () => SearchSource | undefined; - fetch: (options?: any) => Promise; - onRequestStart: (handler: (searchSource: SearchSource, options: any) => void) => void; - getSearchRequestBody: () => any; - destroy: () => void; - history: any[]; -} diff --git a/src/legacy/ui/public/courier/search_source/search_source.js b/src/legacy/ui/public/courier/search_source/search_source.js deleted file mode 100644 index bc69e862fea48..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.js +++ /dev/null @@ -1,540 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * @name SearchSource - * - * @description A promise-based stream of search results that can inherit from other search sources. - * - * Because filters/queries in Kibana have different levels of persistence and come from different - * places, it is important to keep track of where filters come from for when they are saved back to - * the savedObject store in the Kibana index. To do this, we create trees of searchSource objects - * that can have associated query parameters (index, query, filter, etc) which can also inherit from - * other searchSource objects. - * - * At query time, all of the searchSource objects that have subscribers are "flattened", at which - * point the query params from the searchSource are collected while traversing up the inheritance - * chain. At each link in the chain a decision about how to merge the query params is made until a - * single set of query parameters is created for each active searchSource (a searchSource with - * subscribers). - * - * That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy - * works in Kibana. - * - * Visualize, starting from a new search: - * - * - the `savedVis.searchSource` is set as the `appSearchSource`. - * - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is - * upgraded to inherit from the `rootSearchSource`. - * - Any interaction with the visualization will still apply filters to the `appSearchSource`, so - * they will be stored directly on the `savedVis.searchSource`. - * - Any interaction with the time filter will be written to the `rootSearchSource`, so those - * filters will not be saved by the `savedVis`. - * - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are - * defined on it directly, but none of the ones that it inherits from other places. - * - * Visualize, starting from an existing search: - * - * - The `savedVis` loads the `savedSearch` on which it is built. - * - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as - * the `appSearchSource`. - * - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`. - * - Then the `savedVis` is written to elasticsearch it will be flattened and only include the - * filters created in the visualize application and will reconnect the filters from the - * `savedSearch` at runtime to prevent losing the relationship - * - * Dashboard search sources: - * - * - Each panel in a dashboard has a search source. - * - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`. - * - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from - * the dashboard search source. - * - When a filter is added to the search box, or via a visualization, it is written to the - * `appSearchSource`. - */ - -import _ from 'lodash'; -import angular from 'angular'; - -import { normalizeSortRequest } from './_normalize_sort_request'; - -import { fetchSoon } from '../fetch'; -import { fieldWildcardFilter } from '../../field_wildcard'; -import { getHighlightRequest, esQuery } from '../../../../../plugins/data/public'; -import { npSetup } from 'ui/new_platform'; -import chrome from '../../chrome'; -import { RequestFailure } from '../fetch/errors'; -import { filterDocvalueFields } from './filter_docvalue_fields'; - -const FIELDS = [ - 'type', - 'query', - 'filter', - 'sort', - 'highlight', - 'highlightAll', - 'aggs', - 'from', - 'searchAfter', - 'size', - 'source', - 'version', - 'fields', - 'index', -]; - -function parseInitialFields(initialFields) { - if (!initialFields) { - return {}; - } - - return typeof initialFields === 'string' ? - JSON.parse(initialFields) - : _.cloneDeep(initialFields); -} - -function isIndexPattern(val) { - return Boolean(val && typeof val.title === 'string'); -} - -const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout'); -const config = npSetup.core.uiSettings; -const getConfig = (...args) => config.get(...args); -const forIp = Symbol('for which index pattern?'); - -export class SearchSource { - constructor(initialFields) { - this._id = _.uniqueId('data_source'); - - this._searchStrategyId = undefined; - this._fields = parseInitialFields(initialFields); - this._parent = undefined; - - this.history = []; - this._requestStartHandlers = []; - this._inheritOptions = {}; - } - - /***** - * PUBLIC API - *****/ - - setPreferredSearchStrategyId(searchStrategyId) { - this._searchStrategyId = searchStrategyId; - } - - getPreferredSearchStrategyId() { - return this._searchStrategyId; - } - - setFields(newFields) { - this._fields = newFields; - return this; - } - - setField(field, value) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't set field '${field}' on SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - if (field === 'index') { - const fields = this._fields; - - const hasSource = fields.source; - const sourceCameFromIp = hasSource && fields.source.hasOwnProperty(forIp); - const sourceIsForOurIp = sourceCameFromIp && fields.source[forIp] === fields.index; - if (sourceIsForOurIp) { - delete fields.source; - } - - if (value === null || value === undefined) { - delete fields.index; - return this; - } - - if (!isIndexPattern(value)) { - throw new TypeError('expected indexPattern to be an IndexPattern duck.'); - } - - fields[field] = value; - if (!fields.source) { - // imply source filtering based on the index pattern, but allow overriding - // it by simply setting another field for "source". When index is changed - fields.source = function () { - return value.getSourceFiltering(); - }; - fields.source[forIp] = value; - } - - return this; - } - - if (value == null) { - delete this._fields[field]; - return this; - } - - this._fields[field] = value; - return this; - } - - getId() { - return this._id; - } - - getFields() { - return _.clone(this._fields); - } - - /** - * Get fields from the fields - */ - getField(field) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't get field '${field}' from SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - let searchSource = this; - - while (searchSource) { - const value = searchSource._fields[field]; - if (value !== void 0) { - return value; - } - - searchSource = searchSource.getParent(); - } - } - - /** - * Get the field from our own fields, don't traverse up the chain - */ - getOwnField(field) { - if (!FIELDS.includes(field)) { - throw new Error(`Can't get field '${field}' from SearchSource. Acceptable fields are: ${FIELDS.join(', ')}.`); - } - - const value = this._fields[field]; - if (value !== void 0) { - return value; - } - } - - create() { - return new SearchSource(); - } - - createCopy() { - const json = angular.toJson(this._fields); - const newSearchSource = new SearchSource(json); - // when serializing the internal fields we lose the internal classes used in the index - // pattern, so we have to set it again to workaround this behavior - newSearchSource.setField('index', this.getField('index')); - newSearchSource.setParent(this.getParent()); - return newSearchSource; - } - - createChild(options = {}) { - const childSearchSource = new SearchSource(); - childSearchSource.setParent(this, options); - return childSearchSource; - } - - /** - * Set a searchSource that this source should inherit from - * @param {SearchSource} searchSource - the parent searchSource - * @return {this} - chainable - */ - setParent(parent, options = {}) { - this._parent = parent; - this._inheritOptions = options; - return this; - } - - /** - * Get the parent of this SearchSource - * @return {undefined|searchSource} - */ - getParent() { - return this._parent || undefined; - } - - /** - * Fetch this source and reject the returned Promise on error - * - * @async - */ - async fetch(options) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const es = $injector.get('es'); - - await this.requestIsStarting(options); - - const searchRequest = await this._flatten(); - this.history = [searchRequest]; - - const response = await fetchSoon(searchRequest, { - ...(this._searchStrategyId && { searchStrategyId: this._searchStrategyId }), - ...options, - }, { es, config, esShardTimeout }); - - if (response.error) { - throw new RequestFailure(null, response); - } - - return response; - } - - /** - * Add a handler that will be notified whenever requests start - * @param {Function} handler - * @return {undefined} - */ - onRequestStart(handler) { - this._requestStartHandlers.push(handler); - } - - /** - * Called by requests of this search source when they are started - * @param {Courier.Request} request - * @param options - * @return {Promise} - */ - requestIsStarting(options) { - const handlers = [...this._requestStartHandlers]; - // If callparentStartHandlers has been set to true, we also call all - // handlers of parent search sources. - if (this._inheritOptions.callParentStartHandlers) { - let searchSource = this.getParent(); - while (searchSource) { - handlers.push(...searchSource._requestStartHandlers); - searchSource = searchSource.getParent(); - } - } - - return Promise.all(handlers.map(fn => fn(this, options))); - } - - async getSearchRequestBody() { - const searchRequest = await this._flatten(); - return searchRequest.body; - } - - /** - * Completely destroy the SearchSource. - * @return {undefined} - */ - destroy() { - this._requestStartHandlers.length = 0; - } - - /****** - * PRIVATE APIS - ******/ - - /** - * Used to merge properties into the data within ._flatten(). - * The data is passed in and modified by the function - * - * @param {object} data - the current merged data - * @param {*} val - the value at `key` - * @param {*} key - The key of `val` - * @return {undefined} - */ - _mergeProp(data, val, key) { - if (typeof val === 'function') { - const source = this; - return Promise.resolve(val(this)) - .then(function (newVal) { - return source._mergeProp(data, newVal, key); - }); - } - - if (val == null || !key || !_.isString(key)) return; - - switch (key) { - case 'filter': - const filters = Array.isArray(val) ? val : [val]; - data.filters = [...(data.filters || []), ...filters]; - return; - case 'index': - case 'type': - case 'id': - case 'highlightAll': - if (key && data[key] == null) { - data[key] = val; - } - return; - case 'searchAfter': - key = 'search_after'; - addToBody(); - break; - case 'source': - key = '_source'; - addToBody(); - break; - case 'sort': - val = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); - addToBody(); - break; - case 'query': - data.query = (data.query || []).concat(val); - break; - case 'fields': - data[key] = _.uniq([...(data[key] || []), ...val]); - break; - default: - addToBody(); - } - - /** - * Add the key and val to the body of the request - */ - function addToBody() { - data.body = data.body || {}; - // ignore if we already have a value - if (data.body[key] == null) { - data.body[key] = val; - } - } - } - - /** - * Walk the inheritance chain of a source and return it's - * flat representation (taking into account merging rules) - * @returns {Promise} - * @resolved {Object|null} - the flat data of the SearchSource - */ - _flatten() { - // the merged data of this dataSource and it's ancestors - const flatData = {}; - - // function used to write each property from each data object in the chain to flat data - const root = this; - - // start the chain at this source - let current = this; - - // call the ittr and return it's promise - return (function ittr() { - // iterate the _fields object (not array) and - // pass each key:value pair to source._mergeProp. if _mergeProp - // returns a promise, then wait for it to complete and call _mergeProp again - return Promise.all(_.map(current._fields, function ittr(value, key) { - if (value instanceof Promise) { - return value.then(function (value) { - return ittr(value, key); - }); - } - - const prom = root._mergeProp(flatData, value, key); - return prom instanceof Promise ? prom : null; - })) - .then(function () { - // move to this sources parent - const parent = current.getParent(); - // keep calling until we reach the top parent - if (parent) { - current = parent; - return ittr(); - } - }); - }()) - .then(function () { - // This is down here to prevent the circular dependency - flatData.body = flatData.body || {}; - - const computedFields = flatData.index.getComputedFields(); - - flatData.body.stored_fields = computedFields.storedFields; - flatData.body.script_fields = flatData.body.script_fields || {}; - _.extend(flatData.body.script_fields, computedFields.scriptFields); - - const defaultDocValueFields = computedFields.docvalueFields ? computedFields.docvalueFields : []; - flatData.body.docvalue_fields = flatData.body.docvalue_fields || defaultDocValueFields; - - if (flatData.body._source) { - // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(flatData.body._source.excludes, config.get('metaFields')); - flatData.body.docvalue_fields = flatData.body.docvalue_fields.filter( - docvalueField => filter(docvalueField.field) - ); - } - - // if we only want to search for certain fields - const fields = flatData.fields; - if (fields) { - // filter out the docvalue_fields, and script_fields to only include those that we are concerned with - flatData.body.docvalue_fields = filterDocvalueFields(flatData.body.docvalue_fields, fields); - flatData.body.script_fields = _.pick(flatData.body.script_fields, fields); - - // request the remaining fields from both stored_fields and _source - const remainingFields = _.difference(fields, _.keys(flatData.body.script_fields)); - flatData.body.stored_fields = remainingFields; - _.set(flatData.body, '_source.includes', remainingFields); - } - - const esQueryConfigs = esQuery.getEsQueryConfig(config); - flatData.body.query = esQuery.buildEsQuery(flatData.index, flatData.query, flatData.filters, esQueryConfigs); - - if (flatData.highlightAll != null) { - if (flatData.highlightAll && flatData.body.query) { - flatData.body.highlight = getHighlightRequest(flatData.body.query, getConfig); - } - delete flatData.highlightAll; - } - - /** - * Translate a filter into a query to support es 3+ - * @param {Object} filter - The filter to translate - * @return {Object} the query version of that filter - */ - const translateToQuery = function (filter) { - if (!filter) return; - - if (filter.query) { - return filter.query; - } - - return filter; - }; - - // re-write filters within filter aggregations - (function recurse(aggBranch) { - if (!aggBranch) return; - Object.keys(aggBranch).forEach(function (id) { - const agg = aggBranch[id]; - - if (agg.filters) { - // translate filters aggregations - const filters = agg.filters.filters; - - Object.keys(filters).forEach(function (filterId) { - filters[filterId] = translateToQuery(filters[filterId]); - }); - } - - recurse(agg.aggs || agg.aggregations); - }); - }(flatData.body.aggs || flatData.body.aggregations)); - - return flatData; - }); - } -} diff --git a/src/legacy/ui/public/courier/search_source/search_source.test.js b/src/legacy/ui/public/courier/search_source/search_source.test.js deleted file mode 100644 index 800f4e4308671..0000000000000 --- a/src/legacy/ui/public/courier/search_source/search_source.test.js +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SearchSource } from '../search_source'; - -jest.mock('ui/new_platform', () => ({ - npSetup: { - core: { - injectedMetadata: { - getInjectedVar: () => 0, - } - } - } -})); - -jest.mock('../fetch', () => ({ - fetchSoon: jest.fn(), -})); - -const indexPattern = { title: 'foo' }; -const indexPattern2 = { title: 'foo' }; - -describe('SearchSource', function () { - describe('#setField()', function () { - it('sets the value for the property', function () { - const searchSource = new SearchSource(); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); - - it('throws an error if the property is not accepted', function () { - const searchSource = new SearchSource(); - expect(() => searchSource.setField('index', 5)).toThrow(); - }); - }); - - describe('#getField()', function () { - it('gets the value for the property', function () { - const searchSource = new SearchSource(); - searchSource.setField('aggs', 5); - expect(searchSource.getField('aggs')).toBe(5); - }); - - it('throws an error if the property is not accepted', function () { - const searchSource = new SearchSource(); - expect(() => searchSource.getField('unacceptablePropName')).toThrow(); - }); - }); - - describe(`#setField('index')`, function () { - describe('auto-sourceFiltering', function () { - describe('new index pattern assigned', function () { - it('generates a searchSource filter', function () { - const searchSource = new SearchSource(); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(typeof searchSource.getField('source')).toBe('function'); - }); - - it('removes created searchSource filter on removal', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - }); - }); - - describe('new index pattern assigned over another', function () { - it('replaces searchSource filter with new', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - const searchSourceFilter1 = searchSource.getField('source'); - searchSource.setField('index', indexPattern2); - expect(searchSource.getField('index')).toBe(indexPattern2); - expect(typeof searchSource.getField('source')).toBe('function'); - expect(searchSource.getField('source')).not.toBe(searchSourceFilter1); - }); - - it('removes created searchSource filter on removal', function () { - const searchSource = new SearchSource(); - searchSource.setField('index', indexPattern); - searchSource.setField('index', indexPattern2); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(undefined); - }); - }); - - describe('ip assigned before custom searchSource filter', function () { - it('custom searchSource filter becomes new searchSource', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('index', indexPattern); - expect(typeof searchSource.getField('source')).toBe('function'); - searchSource.setField('source', football); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(searchSource.getField('source')).toBe(football); - }); - - it('custom searchSource stays after removal', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('index', indexPattern); - searchSource.setField('source', football); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(football); - }); - }); - - describe('ip assigned after custom searchSource filter', function () { - it('leaves the custom filter in place', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('source', football); - searchSource.setField('index', indexPattern); - expect(searchSource.getField('index')).toBe(indexPattern); - expect(searchSource.getField('source')).toBe(football); - }); - - it('custom searchSource stays after removal', function () { - const searchSource = new SearchSource(); - const football = {}; - searchSource.setField('source', football); - searchSource.setField('index', indexPattern); - searchSource.setField('index', null); - expect(searchSource.getField('index')).toBe(undefined); - expect(searchSource.getField('source')).toBe(football); - }); - }); - }); - }); - - describe('#onRequestStart()', () => { - it('should be called when starting a request', () => { - const searchSource = new SearchSource(); - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const options = {}; - searchSource.requestIsStarting(options); - expect(fn).toBeCalledWith(searchSource, options); - }); - - it('should not be called on parent searchSource', () => { - const parent = new SearchSource(); - const searchSource = new SearchSource().setParent(parent); - - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const parentFn = jest.fn(); - parent.onRequestStart(parentFn); - const options = {}; - searchSource.requestIsStarting(options); - - expect(fn).toBeCalledWith(searchSource, options); - expect(parentFn).not.toBeCalled(); - }); - - it('should be called on parent searchSource if callParentStartHandlers is true', () => { - const parent = new SearchSource(); - const searchSource = new SearchSource().setParent(parent, { callParentStartHandlers: true }); - - const fn = jest.fn(); - searchSource.onRequestStart(fn); - const parentFn = jest.fn(); - parent.onRequestStart(parentFn); - const options = {}; - searchSource.requestIsStarting(options); - - expect(fn).toBeCalledWith(searchSource, options); - expect(parentFn).toBeCalledWith(searchSource, options); - }); - }); -}); diff --git a/src/legacy/ui/public/courier/search_source/search_source.test.ts b/src/legacy/ui/public/courier/search_source/search_source.test.ts new file mode 100644 index 0000000000000..ddd3717f55e29 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/search_source.test.ts @@ -0,0 +1,156 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchSource } from '../search_source'; +import { IndexPattern } from '../../../../core_plugins/data/public'; + +jest.mock('ui/new_platform'); + +jest.mock('../fetch', () => ({ + fetchSoon: jest.fn().mockResolvedValue({}), +})); + +jest.mock('../../chrome', () => ({ + dangerouslyGetActiveInjector: () => ({ + get: jest.fn(), + }), +})); + +const getComputedFields = () => ({ + storedFields: [], + scriptFields: [], + docvalueFields: [], +}); +const mockSource = { excludes: ['foo-*'] }; +const mockSource2 = { excludes: ['bar-*'] }; +const indexPattern = ({ + title: 'foo', + getComputedFields, + getSourceFiltering: () => mockSource, +} as unknown) as IndexPattern; +const indexPattern2 = ({ + title: 'foo', + getComputedFields, + getSourceFiltering: () => mockSource2, +} as unknown) as IndexPattern; + +describe('SearchSource', function() { + describe('#setField()', function() { + it('sets the value for the property', function() { + const searchSource = new SearchSource(); + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + }); + + describe('#getField()', function() { + it('gets the value for the property', function() { + const searchSource = new SearchSource(); + searchSource.setField('aggs', 5); + expect(searchSource.getField('aggs')).toBe(5); + }); + }); + + describe(`#setField('index')`, function() { + describe('auto-sourceFiltering', function() { + describe('new index pattern assigned', function() { + it('generates a searchSource filter', async function() { + const searchSource = new SearchSource(); + expect(searchSource.getField('index')).toBe(undefined); + expect(searchSource.getField('source')).toBe(undefined); + searchSource.setField('index', indexPattern); + expect(searchSource.getField('index')).toBe(indexPattern); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource); + }); + + it('removes created searchSource filter on removal', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + + describe('new index pattern assigned over another', function() { + it('replaces searchSource filter with new', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + expect(searchSource.getField('index')).toBe(indexPattern2); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(mockSource2); + }); + + it('removes created searchSource filter on removal', async function() { + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('index', indexPattern2); + searchSource.setField('index', undefined); + const request = await searchSource.getSearchRequestBody(); + expect(request._source).toBe(undefined); + }); + }); + }); + }); + + describe('#onRequestStart()', () => { + it('should be called when starting a request', async () => { + const searchSource = new SearchSource({ index: indexPattern }); + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const options = {}; + await searchSource.fetch(options); + expect(fn).toBeCalledWith(searchSource, options); + }); + + it('should not be called on parent searchSource', async () => { + const parent = new SearchSource(); + const searchSource = new SearchSource({ index: indexPattern }); + + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const parentFn = jest.fn(); + parent.onRequestStart(parentFn); + const options = {}; + await searchSource.fetch(options); + + expect(fn).toBeCalledWith(searchSource, options); + expect(parentFn).not.toBeCalled(); + }); + + it('should be called on parent searchSource if callParentStartHandlers is true', async () => { + const parent = new SearchSource(); + const searchSource = new SearchSource({ index: indexPattern }).setParent(parent, { + callParentStartHandlers: true, + }); + + const fn = jest.fn(); + searchSource.onRequestStart(fn); + const parentFn = jest.fn(); + parent.onRequestStart(parentFn); + const options = {}; + await searchSource.fetch(options); + + expect(fn).toBeCalledWith(searchSource, options); + expect(parentFn).toBeCalledWith(searchSource, options); + }); + }); +}); diff --git a/src/legacy/ui/public/courier/search_source/search_source.ts b/src/legacy/ui/public/courier/search_source/search_source.ts new file mode 100644 index 0000000000000..e862bb1118a74 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/search_source.ts @@ -0,0 +1,410 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * @name SearchSource + * + * @description A promise-based stream of search results that can inherit from other search sources. + * + * Because filters/queries in Kibana have different levels of persistence and come from different + * places, it is important to keep track of where filters come from for when they are saved back to + * the savedObject store in the Kibana index. To do this, we create trees of searchSource objects + * that can have associated query parameters (index, query, filter, etc) which can also inherit from + * other searchSource objects. + * + * At query time, all of the searchSource objects that have subscribers are "flattened", at which + * point the query params from the searchSource are collected while traversing up the inheritance + * chain. At each link in the chain a decision about how to merge the query params is made until a + * single set of query parameters is created for each active searchSource (a searchSource with + * subscribers). + * + * That set of query parameters is then sent to elasticsearch. This is how the filter hierarchy + * works in Kibana. + * + * Visualize, starting from a new search: + * + * - the `savedVis.searchSource` is set as the `appSearchSource`. + * - The `savedVis.searchSource` would normally inherit from the `appSearchSource`, but now it is + * upgraded to inherit from the `rootSearchSource`. + * - Any interaction with the visualization will still apply filters to the `appSearchSource`, so + * they will be stored directly on the `savedVis.searchSource`. + * - Any interaction with the time filter will be written to the `rootSearchSource`, so those + * filters will not be saved by the `savedVis`. + * - When the `savedVis` is saved to elasticsearch, it takes with it all the filters that are + * defined on it directly, but none of the ones that it inherits from other places. + * + * Visualize, starting from an existing search: + * + * - The `savedVis` loads the `savedSearch` on which it is built. + * - The `savedVis.searchSource` is set to inherit from the `saveSearch.searchSource` and set as + * the `appSearchSource`. + * - The `savedSearch.searchSource`, is set to inherit from the `rootSearchSource`. + * - Then the `savedVis` is written to elasticsearch it will be flattened and only include the + * filters created in the visualize application and will reconnect the filters from the + * `savedSearch` at runtime to prevent losing the relationship + * + * Dashboard search sources: + * + * - Each panel in a dashboard has a search source. + * - The `savedDashboard` also has a searchsource, and it is set as the `appSearchSource`. + * - Each panel's search source inherits from the `appSearchSource`, meaning that they inherit from + * the dashboard search source. + * - When a filter is added to the search box, or via a visualization, it is written to the + * `appSearchSource`. + */ + +import _ from 'lodash'; +import { npSetup } from 'ui/new_platform'; +import { normalizeSortRequest } from './normalize_sort_request'; +import { fetchSoon } from '../fetch'; +import { fieldWildcardFilter } from '../../field_wildcard'; +import { getHighlightRequest, esFilters, esQuery } from '../../../../../plugins/data/public'; +import chrome from '../../chrome'; +import { RequestFailure } from '../fetch/errors'; +import { filterDocvalueFields } from './filter_docvalue_fields'; +import { SearchSourceOptions, SearchSourceFields, SearchRequest } from './types'; +import { FetchOptions, ApiCaller } from '../fetch/types'; + +const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout') as number; +const config = npSetup.core.uiSettings; + +export type SearchSourceContract = Pick; + +export class SearchSource { + private id: string = _.uniqueId('data_source'); + private searchStrategyId?: string; + private parent?: SearchSource; + private requestStartHandlers: Array< + (searchSource: SearchSourceContract, options?: FetchOptions) => Promise + > = []; + private inheritOptions: SearchSourceOptions = {}; + public history: SearchRequest[] = []; + + constructor(private fields: SearchSourceFields = {}) {} + + /** *** + * PUBLIC API + *****/ + + setPreferredSearchStrategyId(searchStrategyId: string) { + this.searchStrategyId = searchStrategyId; + } + + setFields(newFields: SearchSourceFields) { + this.fields = newFields; + return this; + } + + setField(field: K, value: SearchSourceFields[K]) { + if (value == null) { + delete this.fields[field]; + } else { + this.fields[field] = value; + } + return this; + } + + getId() { + return this.id; + } + + getFields() { + return { ...this.fields }; + } + + /** + * Get fields from the fields + */ + getField(field: K, recurse = true): SearchSourceFields[K] { + if (!recurse || this.fields[field] !== void 0) { + return this.fields[field]; + } + const parent = this.getParent(); + return parent && parent.getField(field); + } + + /** + * Get the field from our own fields, don't traverse up the chain + */ + getOwnField(field: K): SearchSourceFields[K] { + return this.getField(field, false); + } + + create() { + return new SearchSource(); + } + + createCopy() { + const newSearchSource = new SearchSource(); + newSearchSource.setFields({ ...this.fields }); + // when serializing the internal fields we lose the internal classes used in the index + // pattern, so we have to set it again to workaround this behavior + newSearchSource.setField('index', this.getField('index')); + newSearchSource.setParent(this.getParent()); + return newSearchSource; + } + + createChild(options = {}) { + const childSearchSource = new SearchSource(); + childSearchSource.setParent(this, options); + return childSearchSource; + } + + /** + * Set a searchSource that this source should inherit from + * @param {SearchSource} parent - the parent searchSource + * @param {SearchSourceOptions} options - the inherit options + * @return {this} - chainable + */ + setParent(parent?: SearchSourceContract, options: SearchSourceOptions = {}) { + this.parent = parent as SearchSource; + this.inheritOptions = options; + return this; + } + + /** + * Get the parent of this SearchSource + * @return {undefined|searchSource} + */ + getParent() { + return this.parent; + } + + /** + * Fetch this source and reject the returned Promise on error + * + * @async + */ + async fetch(options: FetchOptions = {}) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const es = $injector.get('es') as ApiCaller; + + await this.requestIsStarting(options); + + const searchRequest = await this.flatten(); + this.history = [searchRequest]; + + const response = await fetchSoon( + searchRequest, + { + ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), + ...options, + }, + { es, config, esShardTimeout } + ); + + if (response.error) { + throw new RequestFailure(null, response); + } + + return response; + } + + /** + * Add a handler that will be notified whenever requests start + * @param {Function} handler + * @return {undefined} + */ + onRequestStart( + handler: (searchSource: SearchSourceContract, options?: FetchOptions) => Promise + ) { + this.requestStartHandlers.push(handler); + } + + async getSearchRequestBody() { + const searchRequest = await this.flatten(); + return searchRequest.body; + } + + /** + * Completely destroy the SearchSource. + * @return {undefined} + */ + destroy() { + this.requestStartHandlers.length = 0; + } + + /** **** + * PRIVATE APIS + ******/ + + /** + * Called by requests of this search source when they are started + * @param {Courier.Request} request + * @param options + * @return {Promise} + */ + private requestIsStarting(options: FetchOptions = {}) { + const handlers = [...this.requestStartHandlers]; + // If callParentStartHandlers has been set to true, we also call all + // handlers of parent search sources. + if (this.inheritOptions.callParentStartHandlers) { + let searchSource = this.getParent(); + while (searchSource) { + handlers.push(...searchSource.requestStartHandlers); + searchSource = searchSource.getParent(); + } + } + + return Promise.all(handlers.map(fn => fn(this, options))); + } + + /** + * Used to merge properties into the data within ._flatten(). + * The data is passed in and modified by the function + * + * @param {object} data - the current merged data + * @param {*} val - the value at `key` + * @param {*} key - The key of `val` + * @return {undefined} + */ + private mergeProp( + data: SearchRequest, + val: SearchSourceFields[K], + key: K + ) { + val = typeof val === 'function' ? val(this) : val; + if (val == null || !key) return; + + const addToRoot = (rootKey: string, value: any) => { + data[rootKey] = value; + }; + + /** + * Add the key and val to the body of the request + */ + const addToBody = (bodyKey: string, value: any) => { + // ignore if we already have a value + if (data.body[bodyKey] == null) { + data.body[bodyKey] = value; + } + }; + + switch (key) { + case 'filter': + return addToRoot('filters', (data.filters || []).concat(val)); + case 'query': + return addToRoot(key, (data[key] || []).concat(val)); + case 'fields': + const fields = _.uniq((data[key] || []).concat(val)); + return addToRoot(key, fields); + case 'index': + case 'type': + case 'highlightAll': + return key && data[key] == null && addToRoot(key, val); + case 'searchAfter': + return addToBody('search_after', val); + case 'source': + return addToBody('_source', val); + case 'sort': + const sort = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); + return addToBody(key, sort); + default: + return addToBody(key, val); + } + } + + /** + * Walk the inheritance chain of a source and return its + * flat representation (taking into account merging rules) + * @returns {Promise} + * @resolved {Object|null} - the flat data of the SearchSource + */ + private mergeProps(root = this, searchRequest: SearchRequest = { body: {} }) { + Object.entries(this.fields).forEach(([key, value]) => { + this.mergeProp(searchRequest, value, key as keyof SearchSourceFields); + }); + if (this.parent) { + this.parent.mergeProps(root, searchRequest); + } + return searchRequest; + } + + private flatten() { + const searchRequest = this.mergeProps(); + + searchRequest.body = searchRequest.body || {}; + const { body, index, fields, query, filters, highlightAll } = searchRequest; + + const computedFields = index ? index.getComputedFields() : {}; + + body.stored_fields = computedFields.storedFields; + body.script_fields = body.script_fields || {}; + _.extend(body.script_fields, computedFields.scriptFields); + + const defaultDocValueFields = computedFields.docvalueFields + ? computedFields.docvalueFields + : []; + body.docvalue_fields = body.docvalue_fields || defaultDocValueFields; + + if (!body.hasOwnProperty('_source') && index) { + body._source = index.getSourceFiltering(); + } + + if (body._source) { + // exclude source fields for this index pattern specified by the user + const filter = fieldWildcardFilter(body._source.excludes, config.get('metaFields')); + body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => + filter(docvalueField.field) + ); + } + + // if we only want to search for certain fields + if (fields) { + // filter out the docvalue_fields, and script_fields to only include those that we are concerned with + body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); + body.script_fields = _.pick(body.script_fields, fields); + + // request the remaining fields from both stored_fields and _source + const remainingFields = _.difference(fields, _.keys(body.script_fields)); + body.stored_fields = remainingFields; + _.set(body, '_source.includes', remainingFields); + } + + const esQueryConfigs = esQuery.getEsQueryConfig(config); + body.query = esQuery.buildEsQuery(index, query, filters, esQueryConfigs); + + if (highlightAll && body.query) { + body.highlight = getHighlightRequest(body.query, config.get('doc_table:highlight')); + delete searchRequest.highlightAll; + } + + const translateToQuery = (filter: esFilters.Filter) => filter && (filter.query || filter); + + // re-write filters within filter aggregations + (function recurse(aggBranch) { + if (!aggBranch) return; + Object.keys(aggBranch).forEach(function(id) { + const agg = aggBranch[id]; + + if (agg.filters) { + // translate filters aggregations + const { filters: aggFilters } = agg.filters; + Object.keys(aggFilters).forEach(filterId => { + aggFilters[filterId] = translateToQuery(aggFilters[filterId]); + }); + } + + recurse(agg.aggs || agg.aggregations); + }); + })(body.aggs || body.aggregations); + + return searchRequest; + } +} diff --git a/src/legacy/ui/public/courier/search_source/types.ts b/src/legacy/ui/public/courier/search_source/types.ts new file mode 100644 index 0000000000000..293f3d49596c3 --- /dev/null +++ b/src/legacy/ui/public/courier/search_source/types.ts @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { NameList } from 'elasticsearch'; +import { esFilters, Query } from '../../../../../plugins/data/public'; +import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns'; + +export type EsQuerySearchAfter = [string | number, string | number]; + +export enum SortDirection { + asc = 'asc', + desc = 'desc', +} + +export type EsQuerySortValue = Record; + +export interface SearchSourceFields { + type?: string; + query?: Query; + filter?: + | esFilters.Filter[] + | esFilters.Filter + | (() => esFilters.Filter[] | esFilters.Filter | undefined); + sort?: EsQuerySortValue | EsQuerySortValue[]; + highlight?: any; + highlightAll?: boolean; + aggs?: any; + from?: number; + size?: number; + source?: NameList; + version?: boolean; + fields?: NameList; + index?: IndexPattern; + searchAfter?: EsQuerySearchAfter; +} + +export interface SearchSourceOptions { + callParentStartHandlers?: boolean; +} + +export { SearchSourceContract } from './search_source'; + +export interface SortOptions { + mode?: 'min' | 'max' | 'sum' | 'avg' | 'median'; + type?: 'double' | 'long' | 'date' | 'date_nanos'; + nested?: object; + unmapped_type?: string; + distance_type?: 'arc' | 'plane'; + unit?: string; + ignore_unmapped?: boolean; + _script?: object; +} + +export interface Request { + docvalue_fields: string[]; + _source: unknown; + query: unknown; + script_fields: unknown; + sort: unknown; + stored_fields: string[]; +} + +export interface ResponseWithShardFailure { + _shards: { + failed: number; + failures: ShardFailure[]; + skipped: number; + successful: number; + total: number; + }; +} + +export interface ShardFailure { + index: string; + node: string; + reason: { + caused_by: { + reason: string; + type: string; + }; + reason: string; + lang?: string; + script?: string; + script_stack?: string[]; + type: string; + }; + shard: number; +} + +export type SearchRequest = any; +export type SearchResponse = any; diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts similarity index 67% rename from src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js rename to src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts index a1ea53e8b5b47..29921fc7a11d3 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.ts @@ -18,26 +18,28 @@ */ import { defaultSearchStrategy } from './default_search_strategy'; +import { UiSettingsClientContract } from '../../../../../core/public'; +import { SearchStrategySearchParams } from './types'; const { search } = defaultSearchStrategy; -function getConfigStub(config = {}) { +function getConfigStub(config: any = {}) { return { - get: key => config[key] - }; + get: key => config[key], + } as UiSettingsClientContract; } -const msearchMockResponse = Promise.resolve([]); +const msearchMockResponse: any = Promise.resolve([]); msearchMockResponse.abort = jest.fn(); const msearchMock = jest.fn().mockReturnValue(msearchMockResponse); -const searchMockResponse = Promise.resolve([]); +const searchMockResponse: any = Promise.resolve([]); searchMockResponse.abort = jest.fn(); const searchMock = jest.fn().mockReturnValue(searchMockResponse); -describe('defaultSearchStrategy', function () { - describe('search', function () { - let searchArgs; +describe('defaultSearchStrategy', function() { + describe('search', function() { + let searchArgs: MockedKeys>; beforeEach(() => { msearchMockResponse.abort.mockClear(); @@ -47,9 +49,12 @@ describe('defaultSearchStrategy', function () { searchMock.mockClear(); searchArgs = { - searchRequests: [{ - index: { title: 'foo' } - }], + searchRequests: [ + { + index: { title: 'foo' }, + }, + ], + esShardTimeout: 0, es: { msearch: msearchMock, search: searchMock, @@ -58,48 +63,48 @@ describe('defaultSearchStrategy', function () { }); test('does not send max_concurrent_shard_requests by default', async () => { - searchArgs.config = getConfigStub({ 'courier:batchSearches': true }); - await search(searchArgs); + const config = getConfigStub({ 'courier:batchSearches': true }); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); }); test('allows configuration of max_concurrent_shard_requests', async () => { - searchArgs.config = getConfigStub({ + const config = getConfigStub({ 'courier:batchSearches': true, 'courier:maxConcurrentShardRequests': 42, }); - await search(searchArgs); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); }); test('should set rest_total_hits_as_int to true on a request', async () => { - searchArgs.config = getConfigStub({ 'courier:batchSearches': true }); - await search(searchArgs); + const config = getConfigStub({ 'courier:batchSearches': true }); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); }); test('should set ignore_throttled=false when including frozen indices', async () => { - searchArgs.config = getConfigStub({ + const config = getConfigStub({ 'courier:batchSearches': true, 'search:includeFrozen': true, }); - await search(searchArgs); + await search({ ...searchArgs, config }); expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); }); test('should properly call abort with msearch', () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': true + const config = getConfigStub({ + 'courier:batchSearches': true, }); - search(searchArgs).abort(); + search({ ...searchArgs, config }).abort(); expect(msearchMockResponse.abort).toHaveBeenCalled(); }); test('should properly abort with search', async () => { - searchArgs.config = getConfigStub({ - 'courier:batchSearches': false + const config = getConfigStub({ + 'courier:batchSearches': false, }); - search(searchArgs).abort(); + search({ ...searchArgs, config }).abort(); expect(searchMockResponse.abort).toHaveBeenCalled(); }); }); diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts similarity index 76% rename from src/legacy/ui/public/courier/search_strategy/default_search_strategy.js rename to src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts index 42a9b64136454..5be4fef076655 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.ts @@ -17,37 +17,39 @@ * under the License. */ +import { SearchStrategyProvider, SearchStrategySearchParams } from './types'; import { addSearchStrategy } from './search_strategy_registry'; import { isDefaultTypeIndexPattern } from './is_default_type_index_pattern'; -import { getSearchParams, getMSearchParams, getPreference, getTimeout } from '../fetch/get_search_params'; +import { + getSearchParams, + getMSearchParams, + getPreference, + getTimeout, +} from '../fetch/get_search_params'; -export const defaultSearchStrategy = { +export const defaultSearchStrategy: SearchStrategyProvider = { id: 'default', search: params => { return params.config.get('courier:batchSearches') ? msearch(params) : search(params); }, - isViable: (indexPattern) => { - if (!indexPattern) { - return false; - } - - return isDefaultTypeIndexPattern(indexPattern); + isViable: indexPattern => { + return indexPattern && isDefaultTypeIndexPattern(indexPattern); }, }; -function msearch({ searchRequests, es, config, esShardTimeout }) { +function msearch({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { const inlineHeader = { index: index.title || index, search_type: searchType, ignore_unavailable: true, - preference: getPreference(config) + preference: getPreference(config), }; const inlineBody = { ...body, - timeout: getTimeout(esShardTimeout) + timeout: getTimeout(esShardTimeout), }; return `${JSON.stringify(inlineHeader)}\n${JSON.stringify(inlineBody)}`; }); @@ -58,11 +60,11 @@ function msearch({ searchRequests, es, config, esShardTimeout }) { }); return { searching: searching.then(({ responses }) => responses), - abort: searching.abort + abort: searching.abort, }; } -function search({ searchRequests, es, config, esShardTimeout }) { +function search({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { const abortController = new AbortController(); const searchParams = getSearchParams(config, esShardTimeout); const promises = searchRequests.map(({ index, body }) => { diff --git a/src/legacy/ui/public/courier/search_strategy/index.js b/src/legacy/ui/public/courier/search_strategy/index.ts similarity index 100% rename from src/legacy/ui/public/courier/search_strategy/index.js rename to src/legacy/ui/public/courier/search_strategy/index.ts diff --git a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts similarity index 85% rename from src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js rename to src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts index 94c85c0e13ec7..3785ce6341078 100644 --- a/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.js +++ b/src/legacy/ui/public/courier/search_strategy/is_default_type_index_pattern.ts @@ -17,7 +17,9 @@ * under the License. */ -export const isDefaultTypeIndexPattern = indexPattern => { +import { IndexPattern } from '../../../../core_plugins/data/public'; + +export const isDefaultTypeIndexPattern = (indexPattern: IndexPattern) => { // Default index patterns don't have `type` defined. return !indexPattern.type; }; diff --git a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts similarity index 79% rename from src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js rename to src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts index c4499cc870d56..24c3876cfcc05 100644 --- a/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.js +++ b/src/legacy/ui/public/courier/search_strategy/no_op_search_strategy.ts @@ -17,21 +17,25 @@ * under the License. */ -import { SearchError } from './search_error'; import { i18n } from '@kbn/i18n'; +import { SearchError } from './search_error'; +import { SearchStrategyProvider } from './types'; -export const noOpSearchStrategy = { +export const noOpSearchStrategy: SearchStrategyProvider = { id: 'noOp', - search: async () => { + search: () => { const searchError = new SearchError({ status: '418', // "I'm a teapot" error title: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageTitle', { defaultMessage: 'No search strategy registered', }), - message: i18n.translate('common.ui.courier.noSearchStrategyRegisteredErrorMessageDescription', { - defaultMessage: `Couldn't find a search strategy for the search request`, - }), + message: i18n.translate( + 'common.ui.courier.noSearchStrategyRegisteredErrorMessageDescription', + { + defaultMessage: `Couldn't find a search strategy for the search request`, + } + ), type: 'NO_OP_SEARCH_STRATEGY', path: '', }); @@ -39,7 +43,6 @@ export const noOpSearchStrategy = { return { searching: Promise.reject(searchError), abort: () => {}, - failedSearchRequests: [], }; }, diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.d.ts b/src/legacy/ui/public/courier/search_strategy/search_error.d.ts deleted file mode 100644 index bf49853957c75..0000000000000 --- a/src/legacy/ui/public/courier/search_strategy/search_error.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type SearchError = any; -export type getSearchErrorType = any; diff --git a/src/legacy/ui/public/courier/search_strategy/search_error.js b/src/legacy/ui/public/courier/search_strategy/search_error.ts similarity index 76% rename from src/legacy/ui/public/courier/search_strategy/search_error.js rename to src/legacy/ui/public/courier/search_strategy/search_error.ts index 9c35d11a6abf4..d4042fb17499c 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_error.js +++ b/src/legacy/ui/public/courier/search_strategy/search_error.ts @@ -17,8 +17,23 @@ * under the License. */ +interface SearchErrorOptions { + status: string; + title: string; + message: string; + path: string; + type: string; +} + export class SearchError extends Error { - constructor({ status, title, message, path, type }) { + public name: string; + public status: string; + public title: string; + public message: string; + public path: string; + public type: string; + + constructor({ status, title, message, path, type }: SearchErrorOptions) { super(message); this.name = 'SearchError'; this.status = status; @@ -39,9 +54,9 @@ export class SearchError extends Error { } } -export function getSearchErrorType({ message }) { +export function getSearchErrorType({ message }: Pick) { const msg = message.toLowerCase(); - if(msg.indexOf('unsupported query') > -1) { + if (msg.indexOf('unsupported query') > -1) { return 'UNSUPPORTED_QUERY'; } } diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts similarity index 58% rename from src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js rename to src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts index 362d303eb6203..ae2ed6128c8ea 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.js +++ b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { IndexPattern } from '../../../../core_plugins/data/public'; import { noOpSearchStrategy } from './no_op_search_strategy'; import { searchStrategies, @@ -24,16 +25,28 @@ import { getSearchStrategyByViability, getSearchStrategyById, getSearchStrategyForSearchRequest, - hasSearchStategyForIndexPattern + hasSearchStategyForIndexPattern, } from './search_strategy_registry'; - -const mockSearchStrategies = [{ - id: 0, - isViable: index => index === 0 -}, { - id: 1, - isViable: index => index === 1 -}]; +import { SearchStrategyProvider } from './types'; + +const mockSearchStrategies: SearchStrategyProvider[] = [ + { + id: '0', + isViable: (index: IndexPattern) => index.id === '0', + search: () => ({ + searching: Promise.resolve([]), + abort: () => void 0, + }), + }, + { + id: '1', + isViable: (index: IndexPattern) => index.id === '1', + search: () => ({ + searching: Promise.resolve([]), + abort: () => void 0, + }), + }, +]; describe('Search strategy registry', () => { beforeEach(() => { @@ -59,12 +72,16 @@ describe('Search strategy registry', () => { }); it('returns the viable strategy', () => { - expect(getSearchStrategyByViability(0)).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyByViability(1)).toBe(mockSearchStrategies[1]); + expect(getSearchStrategyByViability({ id: '0' } as IndexPattern)).toBe( + mockSearchStrategies[0] + ); + expect(getSearchStrategyByViability({ id: '1' } as IndexPattern)).toBe( + mockSearchStrategies[1] + ); }); it('returns undefined if there is no viable strategy', () => { - expect(getSearchStrategyByViability(-1)).toBe(undefined); + expect(getSearchStrategyByViability({ id: '-1' } as IndexPattern)).toBe(undefined); }); }); @@ -74,12 +91,16 @@ describe('Search strategy registry', () => { }); it('returns the strategy by ID', () => { - expect(getSearchStrategyById(0)).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyById(1)).toBe(mockSearchStrategies[1]); + expect(getSearchStrategyById('0')).toBe(mockSearchStrategies[0]); + expect(getSearchStrategyById('1')).toBe(mockSearchStrategies[1]); }); it('returns undefined if there is no strategy with that ID', () => { - expect(getSearchStrategyById(-1)).toBe(undefined); + expect(getSearchStrategyById('-1')).toBe(undefined); + }); + + it('returns the noOp search strategy if passed that ID', () => { + expect(getSearchStrategyById('noOp')).toBe(noOpSearchStrategy); }); }); @@ -89,15 +110,29 @@ describe('Search strategy registry', () => { }); it('returns the strategy by ID if provided', () => { - expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: 1 })).toBe(mockSearchStrategies[1]); + expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: '1' })).toBe( + mockSearchStrategies[1] + ); + }); + + it('throws if there is no strategy by provided ID', () => { + expect(() => + getSearchStrategyForSearchRequest({}, { searchStrategyId: '-1' }) + ).toThrowErrorMatchingInlineSnapshot(`"No strategy with ID -1"`); }); it('returns the strategy by viability if there is one', () => { - expect(getSearchStrategyForSearchRequest({ index: 1 })).toBe(mockSearchStrategies[1]); + expect( + getSearchStrategyForSearchRequest({ + index: { + id: '1', + }, + }) + ).toBe(mockSearchStrategies[1]); }); it('returns the no op strategy if there is no viable strategy', () => { - expect(getSearchStrategyForSearchRequest({ index: 3 })).toBe(noOpSearchStrategy); + expect(getSearchStrategyForSearchRequest({ index: '3' })).toBe(noOpSearchStrategy); }); }); @@ -107,8 +142,8 @@ describe('Search strategy registry', () => { }); it('returns whether there is a search strategy for this index pattern', () => { - expect(hasSearchStategyForIndexPattern(0)).toBe(true); - expect(hasSearchStategyForIndexPattern(-1)).toBe(false); + expect(hasSearchStategyForIndexPattern({ id: '0' } as IndexPattern)).toBe(true); + expect(hasSearchStategyForIndexPattern({ id: '-1' } as IndexPattern)).toBe(false); }); }); }); diff --git a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts similarity index 64% rename from src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js rename to src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts index e67d39ea27aa6..9ef007f97531e 100644 --- a/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.js +++ b/src/legacy/ui/public/courier/search_strategy/search_strategy_registry.ts @@ -17,11 +17,14 @@ * under the License. */ +import { IndexPattern } from '../../../../core_plugins/data/public'; +import { SearchStrategyProvider } from './types'; import { noOpSearchStrategy } from './no_op_search_strategy'; +import { SearchResponse } from '../types'; -export const searchStrategies = []; +export const searchStrategies: SearchStrategyProvider[] = []; -export const addSearchStrategy = searchStrategy => { +export const addSearchStrategy = (searchStrategy: SearchStrategyProvider) => { if (searchStrategies.includes(searchStrategy)) { return; } @@ -29,22 +32,27 @@ export const addSearchStrategy = searchStrategy => { searchStrategies.push(searchStrategy); }; -export const getSearchStrategyByViability = indexPattern => { +export const getSearchStrategyByViability = (indexPattern: IndexPattern) => { return searchStrategies.find(searchStrategy => { return searchStrategy.isViable(indexPattern); }); }; -export const getSearchStrategyById = searchStrategyId => { - return searchStrategies.find(searchStrategy => { +export const getSearchStrategyById = (searchStrategyId: string) => { + return [...searchStrategies, noOpSearchStrategy].find(searchStrategy => { return searchStrategy.id === searchStrategyId; }); }; -export const getSearchStrategyForSearchRequest = (searchRequest, { searchStrategyId } = {}) => { +export const getSearchStrategyForSearchRequest = ( + searchRequest: SearchResponse, + { searchStrategyId }: { searchStrategyId?: string } = {} +) => { // Allow the searchSource to declare the correct strategy with which to execute its searches. if (searchStrategyId != null) { - return getSearchStrategyById(searchStrategyId); + const strategy = getSearchStrategyById(searchStrategyId); + if (!strategy) throw Error(`No strategy with ID ${searchStrategyId}`); + return strategy; } // Otherwise try to match it to a strategy. @@ -58,6 +66,6 @@ export const getSearchStrategyForSearchRequest = (searchRequest, { searchStrateg return noOpSearchStrategy; }; -export const hasSearchStategyForIndexPattern = indexPattern => { +export const hasSearchStategyForIndexPattern = (indexPattern: IndexPattern) => { return Boolean(getSearchStrategyByViability(indexPattern)); }; diff --git a/src/legacy/ui/public/courier/search_strategy/types.ts b/src/legacy/ui/public/courier/search_strategy/types.ts new file mode 100644 index 0000000000000..1542f9824a5b1 --- /dev/null +++ b/src/legacy/ui/public/courier/search_strategy/types.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../../core_plugins/data/public'; +import { FetchHandlers } from '../fetch/types'; +import { SearchRequest, SearchResponse } from '../types'; + +export interface SearchStrategyProvider { + id: string; + search: (params: SearchStrategySearchParams) => SearchStrategyResponse; + isViable: (indexPattern: IndexPattern) => boolean; +} + +export interface SearchStrategyResponse { + searching: Promise; + abort: () => void; +} + +export interface SearchStrategySearchParams extends FetchHandlers { + searchRequests: SearchRequest[]; +} diff --git a/src/legacy/ui/public/courier/search_strategy/index.d.ts b/src/legacy/ui/public/courier/types.ts similarity index 84% rename from src/legacy/ui/public/courier/search_strategy/index.d.ts rename to src/legacy/ui/public/courier/types.ts index dc98484655d00..23d74ce6a57da 100644 --- a/src/legacy/ui/public/courier/search_strategy/index.d.ts +++ b/src/legacy/ui/public/courier/types.ts @@ -17,4 +17,7 @@ * under the License. */ -export { SearchError, getSearchErrorType } from './search_error'; +export * from './fetch/types'; +export * from './search_source/types'; +export * from './search_strategy/types'; +export * from './utils/types'; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts b/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts deleted file mode 100644 index 7f638d357a9e1..0000000000000 --- a/src/legacy/ui/public/courier/utils/courier_inspector_utils.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SearchSource } from 'ui/courier'; - -interface InspectorStat { - label: string; - value: string; - description: string; -} - -interface RequestInspectorStats { - indexPattern: InspectorStat; - indexPatternId: InspectorStat; -} - -interface ResponseInspectorStats { - queryTime: InspectorStat; - hitsTotal: InspectorStat; - hits: InspectorStat; - requestTime: InspectorStat; -} - -interface Response { - took: number; - hits: { - total: number; - hits: any[]; - }; -} - -export function getRequestInspectorStats(searchSource: SearchSource): RequestInspectorStats; -export function getResponseInspectorStats( - searchSource: SearchSource, - resp: Response -): ResponseInspectorStats; diff --git a/src/legacy/ui/public/courier/utils/courier_inspector_utils.js b/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts similarity index 78% rename from src/legacy/ui/public/courier/utils/courier_inspector_utils.js rename to src/legacy/ui/public/courier/utils/courier_inspector_utils.ts index 0e53f92bd9dcb..2c47fae4cce37 100644 --- a/src/legacy/ui/public/courier/utils/courier_inspector_utils.js +++ b/src/legacy/ui/public/courier/utils/courier_inspector_utils.ts @@ -25,51 +25,57 @@ */ import { i18n } from '@kbn/i18n'; +import { SearchResponse } from 'elasticsearch'; +import { SearchSourceContract, RequestInspectorStats } from '../types'; -function getRequestInspectorStats(searchSource) { - const stats = {}; +function getRequestInspectorStats(searchSource: SearchSourceContract) { + const stats: RequestInspectorStats = {}; const index = searchSource.getField('index'); if (index) { stats.indexPattern = { label: i18n.translate('common.ui.courier.indexPatternLabel', { - defaultMessage: 'Index pattern' + defaultMessage: 'Index pattern', }), value: index.title, description: i18n.translate('common.ui.courier.indexPatternDescription', { - defaultMessage: 'The index pattern that connected to the Elasticsearch indices.' + defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', }), }; stats.indexPatternId = { label: i18n.translate('common.ui.courier.indexPatternIdLabel', { - defaultMessage: 'Index pattern ID' + defaultMessage: 'Index pattern ID', }), - value: index.id, + value: index.id!, description: i18n.translate('common.ui.courier.indexPatternIdDescription', { defaultMessage: 'The ID in the {kibanaIndexPattern} index.', - values: { kibanaIndexPattern: '.kibana' } + values: { kibanaIndexPattern: '.kibana' }, }), }; } return stats; } -function getResponseInspectorStats(searchSource, resp) { +function getResponseInspectorStats( + searchSource: SearchSourceContract, + resp: SearchResponse +) { const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1]; - const stats = {}; + const stats: RequestInspectorStats = {}; if (resp && resp.took) { stats.queryTime = { label: i18n.translate('common.ui.courier.queryTimeLabel', { - defaultMessage: 'Query time' + defaultMessage: 'Query time', }), value: i18n.translate('common.ui.courier.queryTimeValue', { defaultMessage: '{queryTime}ms', values: { queryTime: resp.took }, }), description: i18n.translate('common.ui.courier.queryTimeDescription', { - defaultMessage: 'The time it took to process the query. ' + - 'Does not include the time to send the request or parse it in the browser.' + defaultMessage: + 'The time it took to process the query. ' + + 'Does not include the time to send the request or parse it in the browser.', }), }; } @@ -77,21 +83,21 @@ function getResponseInspectorStats(searchSource, resp) { if (resp && resp.hits) { stats.hitsTotal = { label: i18n.translate('common.ui.courier.hitsTotalLabel', { - defaultMessage: 'Hits (total)' + defaultMessage: 'Hits (total)', }), value: `${resp.hits.total}`, description: i18n.translate('common.ui.courier.hitsTotalDescription', { - defaultMessage: 'The number of documents that match the query.' + defaultMessage: 'The number of documents that match the query.', }), }; stats.hits = { label: i18n.translate('common.ui.courier.hitsLabel', { - defaultMessage: 'Hits' + defaultMessage: 'Hits', }), value: `${resp.hits.hits.length}`, description: i18n.translate('common.ui.courier.hitsDescription', { - defaultMessage: 'The number of documents returned by the query.' + defaultMessage: 'The number of documents returned by the query.', }), }; } @@ -99,15 +105,16 @@ function getResponseInspectorStats(searchSource, resp) { if (lastRequest && (lastRequest.ms === 0 || lastRequest.ms)) { stats.requestTime = { label: i18n.translate('common.ui.courier.requestTimeLabel', { - defaultMessage: 'Request time' + defaultMessage: 'Request time', }), value: i18n.translate('common.ui.courier.requestTimeValue', { defaultMessage: '{requestTime}ms', values: { requestTime: lastRequest.ms }, }), description: i18n.translate('common.ui.courier.requestTimeDescription', { - defaultMessage: 'The time of the request from the browser to Elasticsearch and back. ' + - 'Does not include the time the requested waited in the queue.' + defaultMessage: + 'The time of the request from the browser to Elasticsearch and back. ' + + 'Does not include the time the requested waited in the queue.', }), }; } diff --git a/src/legacy/ui/public/courier/index.js b/src/legacy/ui/public/courier/utils/types.ts similarity index 71% rename from src/legacy/ui/public/courier/index.js rename to src/legacy/ui/public/courier/utils/types.ts index 5647af3d0d645..305f27a86b398 100644 --- a/src/legacy/ui/public/courier/index.js +++ b/src/legacy/ui/public/courier/utils/types.ts @@ -17,12 +17,17 @@ * under the License. */ -export { SearchSource } from './search_source'; +export interface InspectorStat { + label: string; + value: string; + description: string; +} -export { - addSearchStrategy, - hasSearchStategyForIndexPattern, - isDefaultTypeIndexPattern, - SearchError, - getSearchErrorType, -} from './search_strategy'; +export interface RequestInspectorStats { + indexPattern?: InspectorStat; + indexPatternId?: InspectorStat; + queryTime?: InspectorStat; + hitsTotal?: InspectorStat; + hits?: InspectorStat; + requestTime?: InspectorStat; +} diff --git a/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js b/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js deleted file mode 100644 index a15c602b7ba83..0000000000000 --- a/src/legacy/ui/public/field_wildcard/__tests__/field_wildcard.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import { fieldWildcardFilter, makeRegEx } from '../../field_wildcard'; - -describe('fieldWildcard', function () { - const metaFields = ['_id', '_type', '_source']; - - beforeEach(ngMock.module('kibana')); - - describe('makeRegEx', function () { - it('matches * in any position', function () { - expect('aaaaaabbbbbbbcccccc').to.match(makeRegEx('*a*b*c*')); - expect('a1234').to.match(makeRegEx('*1234')); - expect('1234a').to.match(makeRegEx('1234*')); - expect('12a34').to.match(makeRegEx('12a34')); - }); - - it('properly escapes regexp control characters', function () { - expect('account[user_id]').to.match(makeRegEx('account[*]')); - }); - - it('properly limits matches without wildcards', function () { - expect('username').to.match(makeRegEx('*name')); - expect('username').to.match(makeRegEx('user*')); - expect('username').to.match(makeRegEx('username')); - expect('username').to.not.match(makeRegEx('user')); - expect('username').to.not.match(makeRegEx('name')); - expect('username').to.not.match(makeRegEx('erna')); - }); - }); - - describe('filter', function () { - it('filters nothing when given undefined', function () { - const filter = fieldWildcardFilter(); - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(original); - }); - - it('filters nothing when given an empty array', function () { - const filter = fieldWildcardFilter([], metaFields); - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(original); - }); - - it('does not filter metaFields', function () { - const filter = fieldWildcardFilter([ '_*' ], metaFields); - - const original = [ - '_id', - '_type', - '_typefake' - ]; - - expect(original.filter(filter)).to.eql(['_id', '_type']); - }); - - it('filters values that match the globs', function () { - const filter = fieldWildcardFilter([ - 'f*', - '*4' - ], metaFields); - - const original = [ - 'foo', - 'bar', - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql(['bar', 'baz']); - }); - - it('handles weird values okay', function () { - const filter = fieldWildcardFilter([ - 'f*', - '*4', - 'undefined' - ], metaFields); - - const original = [ - 'foo', - null, - 'bar', - undefined, - {}, - [], - 'baz', - 1234 - ]; - - expect(original.filter(filter)).to.eql([null, 'bar', {}, [], 'baz']); - }); - }); -}); diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts b/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts new file mode 100644 index 0000000000000..9f7523866fdc1 --- /dev/null +++ b/src/legacy/ui/public/field_wildcard/field_wildcard.test.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { fieldWildcardFilter, makeRegEx } from './field_wildcard'; + +describe('fieldWildcard', () => { + const metaFields = ['_id', '_type', '_source']; + + describe('makeRegEx', function() { + it('matches * in any position', function() { + expect('aaaaaabbbbbbbcccccc').toMatch(makeRegEx('*a*b*c*')); + expect('a1234').toMatch(makeRegEx('*1234')); + expect('1234a').toMatch(makeRegEx('1234*')); + expect('12a34').toMatch(makeRegEx('12a34')); + }); + + it('properly escapes regexp control characters', function() { + expect('account[user_id]').toMatch(makeRegEx('account[*]')); + }); + + it('properly limits matches without wildcards', function() { + expect('username').toMatch(makeRegEx('*name')); + expect('username').toMatch(makeRegEx('user*')); + expect('username').toMatch(makeRegEx('username')); + expect('username').not.toMatch(makeRegEx('user')); + expect('username').not.toMatch(makeRegEx('name')); + expect('username').not.toMatch(makeRegEx('erna')); + }); + }); + + describe('filter', function() { + it('filters nothing when given undefined', function() { + const filter = fieldWildcardFilter(); + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(val => filter(val))).toEqual(original); + }); + + it('filters nothing when given an empty array', function() { + const filter = fieldWildcardFilter([], metaFields); + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(filter)).toEqual(original); + }); + + it('does not filter metaFields', function() { + const filter = fieldWildcardFilter(['_*'], metaFields); + + const original = ['_id', '_type', '_typefake']; + + expect(original.filter(filter)).toEqual(['_id', '_type']); + }); + + it('filters values that match the globs', function() { + const filter = fieldWildcardFilter(['f*', '*4'], metaFields); + + const original = ['foo', 'bar', 'baz', 1234]; + + expect(original.filter(filter)).toEqual(['bar', 'baz']); + }); + + it('handles weird values okay', function() { + const filter = fieldWildcardFilter(['f*', '*4', 'undefined'], metaFields); + + const original = ['foo', null, 'bar', undefined, {}, [], 'baz', 1234]; + + expect(original.filter(filter)).toEqual([null, 'bar', {}, [], 'baz']); + }); + }); +}); diff --git a/src/legacy/ui/public/field_wildcard/field_wildcard.js b/src/legacy/ui/public/field_wildcard/field_wildcard.ts similarity index 70% rename from src/legacy/ui/public/field_wildcard/field_wildcard.js rename to src/legacy/ui/public/field_wildcard/field_wildcard.ts index 656641b20a98c..5437086ddd6f4 100644 --- a/src/legacy/ui/public/field_wildcard/field_wildcard.js +++ b/src/legacy/ui/public/field_wildcard/field_wildcard.ts @@ -19,25 +19,29 @@ import { escapeRegExp, memoize } from 'lodash'; -export const makeRegEx = memoize(function makeRegEx(glob) { - return new RegExp('^' + glob.split('*').map(escapeRegExp).join('.*') + '$'); +export const makeRegEx = memoize(function makeRegEx(glob: string) { + const globRegex = glob + .split('*') + .map(escapeRegExp) + .join('.*'); + return new RegExp(`^${globRegex}$`); }); // Note that this will return an essentially noop function if globs is undefined. -export function fieldWildcardMatcher(globs = [], metaFields) { - return function matcher(val) { +export function fieldWildcardMatcher(globs: string[] = [], metaFields: unknown[] = []) { + return function matcher(val: unknown) { // do not test metaFields or keyword if (metaFields.indexOf(val) !== -1) { return false; } - return globs.some(p => makeRegEx(p).test(val)); + return globs.some(p => makeRegEx(p).test(`${val}`)); }; } // Note that this will return an essentially noop function if globs is undefined. -export function fieldWildcardFilter(globs = [], metaFields = []) { +export function fieldWildcardFilter(globs: string[] = [], metaFields: string[] = []) { const matcher = fieldWildcardMatcher(globs, metaFields); - return function filter(val) { + return function filter(val: unknown) { return !matcher(val); }; } diff --git a/src/legacy/ui/public/field_wildcard/index.js b/src/legacy/ui/public/field_wildcard/index.ts similarity index 100% rename from src/legacy/ui/public/field_wildcard/index.js rename to src/legacy/ui/public/field_wildcard/index.ts diff --git a/src/legacy/ui/public/promises/defer.ts b/src/legacy/ui/public/promises/defer.ts index 8ef97c0b3ebcc..3d435f2ba8dfd 100644 --- a/src/legacy/ui/public/promises/defer.ts +++ b/src/legacy/ui/public/promises/defer.ts @@ -17,7 +17,7 @@ * under the License. */ -interface Defer { +export interface Defer { promise: Promise; resolve(value: T): void; reject(reason: Error): void; diff --git a/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx b/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx index ebbe886b3650b..19cbbf9cea04c 100644 --- a/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx +++ b/src/legacy/ui/public/visualize/components/visualization_requesterror.tsx @@ -19,7 +19,7 @@ import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; -import { SearchError } from 'ui/courier'; +import { SearchError } from '../../courier'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; interface VisualizationRequestErrorProps { @@ -32,7 +32,7 @@ export class VisualizationRequestError extends React.Component diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts index 70e0c1f1382fa..608a8b9ce8aa7 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts @@ -28,7 +28,7 @@ import { } from './build_pipeline'; import { Vis, VisState } from 'ui/vis'; import { AggConfig } from 'ui/agg_types/agg_config'; -import { searchSourceMock } from 'ui/courier/search_source/mocks'; +import { searchSourceMock } from '../../../courier/search_source/mocks'; jest.mock('ui/new_platform'); jest.mock('ui/agg_types/buckets/date_histogram', () => ({ diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index 21b13abea440e..ca9540b4d3737 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -20,11 +20,11 @@ import { cloneDeep, get } from 'lodash'; // @ts-ignore import { setBounds } from 'ui/agg_types'; -import { SearchSource } from 'ui/courier'; import { AggConfig, Vis, VisParams, VisState } from 'ui/vis'; import { isDateHistogramBucketAggConfig } from 'ui/agg_types/buckets/date_histogram'; import moment from 'moment'; import { SerializedFieldFormat } from 'src/plugins/expressions/public'; +import { SearchSourceContract } from '../../../courier/types'; import { createFormat } from './utilities'; interface SchemaConfigParams { @@ -462,7 +462,7 @@ export const buildVislibDimensions = async ( // take a Vis object and decorate it with the necessary params (dimensions, bucket, metric, etc) export const getVisParams = async ( vis: Vis, - params: { searchSource: SearchSource; timeRange?: any; abortSignal?: AbortSignal } + params: { searchSource: SearchSourceContract; timeRange?: any; abortSignal?: AbortSignal } ) => { const schemas = getSchemas(vis, params.timeRange); let visConfig = cloneDeep(vis.params); @@ -479,7 +479,10 @@ export const getVisParams = async ( export const buildPipeline = async ( vis: Vis, - params: { searchSource: SearchSource; timeRange?: any } + params: { + searchSource: SearchSourceContract; + timeRange?: any; + } ) => { const { searchSource } = params; const { indexPattern } = vis; diff --git a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts index 36759551a1723..a9203415321fa 100644 --- a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts +++ b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts @@ -24,13 +24,13 @@ import { toastNotifications } from 'ui/notify'; import { AggConfig } from 'ui/vis'; import { timefilter } from 'ui/timefilter'; import { Vis } from '../../../vis'; +import { SearchSource, SearchSourceContract } from '../../../courier'; import { esFilters, Query } from '../../../../../../plugins/data/public'; -import { SearchSource } from '../../../courier'; interface QueryGeohashBoundsParams { filters?: esFilters.Filter[]; query?: Query; - searchSource?: SearchSource; + searchSource?: SearchSourceContract; } /** diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index 6a5c7bdf8eea3..6e03c665290ae 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -49,4 +49,11 @@ describe('filterMatchesIndex', () => { expect(filterMatchesIndex(filter, indexPattern)).toBe(false); }); + + it('should return true if the filter has meta without a key', () => { + const filter = { meta: { index: 'foo' } } as Filter; + const indexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; + + expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + }); }); diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 496aab3ea585f..9b68f5088c447 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -26,7 +26,7 @@ import { Filter } from '../filters'; * change. */ export function filterMatchesIndex(filter: Filter, indexPattern: IIndexPattern | null) { - if (!filter.meta || !indexPattern) { + if (!filter.meta?.key || !indexPattern) { return true; } return indexPattern.fields.some((field: IFieldType) => field.name === filter.meta.key); diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index fa07b3e611fa7..3d819bd145fa6 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -63,18 +63,22 @@ export type RangeFilterMeta = FilterMeta & { formattedValue?: string; }; -export type RangeFilter = Filter & { - meta: RangeFilterMeta; - script?: { - script: { - params: any; - lang: string; - source: any; +export interface EsRangeFilter { + range: { [key: string]: RangeFilterParams }; +} + +export type RangeFilter = Filter & + EsRangeFilter & { + meta: RangeFilterMeta; + script?: { + script: { + params: any; + lang: string; + source: any; + }; }; + match_all?: any; }; - match_all?: any; - range: { [key: string]: RangeFilterParams }; -}; export const isRangeFilter = (filter: any): filter is RangeFilter => filter && filter.range; diff --git a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts index 5312f1be6c26c..8788d4b690aba 100644 --- a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts +++ b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.test.ts @@ -20,36 +20,19 @@ import { getHighlightRequest } from './highlight_request'; describe('getHighlightRequest', () => { - let configMock: Record; - const getConfig = (key: string) => configMock[key]; const queryStringQuery = { query_string: { query: 'foo' } }; - beforeEach(function() { - configMock = {}; - configMock['doc_table:highlight'] = true; - }); - test('should be a function', () => { expect(getHighlightRequest).toBeInstanceOf(Function); }); test('should not modify the original query', () => { - getHighlightRequest(queryStringQuery, getConfig); + getHighlightRequest(queryStringQuery, true); expect(queryStringQuery.query_string).not.toHaveProperty('highlight'); }); test('should return undefined if highlighting is turned off', () => { - configMock['doc_table:highlight'] = false; - const request = getHighlightRequest(queryStringQuery, getConfig); - expect(request).toBe(undefined); - }); - - test('should enable/disable highlighting if config is changed', () => { - let request = getHighlightRequest(queryStringQuery, getConfig); - expect(request).not.toBe(undefined); - - configMock['doc_table:highlight'] = false; - request = getHighlightRequest(queryStringQuery, getConfig); + const request = getHighlightRequest(queryStringQuery, false); expect(request).toBe(undefined); }); }); diff --git a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts index 199a73e692e39..8012ab59c33ba 100644 --- a/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts +++ b/src/plugins/data/common/field_formats/utils/highlight/highlight_request.ts @@ -21,8 +21,8 @@ import { highlightTags } from './highlight_tags'; const FRAGMENT_SIZE = Math.pow(2, 31) - 1; // Max allowed value for fragment_size (limit of a java int) -export function getHighlightRequest(query: any, getConfig: Function) { - if (!getConfig('doc_table:highlight')) return; +export function getHighlightRequest(query: any, shouldHighlight: boolean) { + if (!shouldHighlight) return; return { pre_tags: [highlightTags.pre], diff --git a/src/plugins/data/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts index a1eb36c2ee028..a8eb3a3fe8102 100644 --- a/src/plugins/data/public/query/timefilter/get_time.test.ts +++ b/src/plugins/data/public/query/timefilter/get_time.test.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import sinon from 'sinon'; -import { Filter, getTime } from './get_time'; +import { getTime } from './get_time'; describe('get_time', () => { describe('getTime', () => { @@ -43,8 +43,8 @@ describe('get_time', () => { ], } as any, { from: 'now-60y', to: 'now' } - ) as Filter; - expect(filter.range.date).toEqual({ + ); + expect(filter!.range.date).toEqual({ gte: '1940-02-01T00:00:00.000Z', lte: '2000-02-01T00:00:00.000Z', format: 'strict_date_optional_time', diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index 41ad1a49af0ff..d3fbc17734f81 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -21,22 +21,13 @@ import dateMath from '@elastic/datemath'; import { TimeRange } from '../../../common'; // TODO: remove this -import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public/index_patterns'; +import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public'; +import { esFilters } from '../../../common'; interface CalculateBoundsOptions { forceNow?: Date; } -interface RangeFilter { - gte?: string | number; - lte?: string | number; - format: string; -} - -export interface Filter { - range: { [s: string]: RangeFilter }; -} - export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOptions = {}) { return { min: dateMath.parse(timeRange.from, { forceNow: options.forceNow }), @@ -45,10 +36,10 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp } export function getTime( - indexPattern: IndexPattern, + indexPattern: IndexPattern | undefined, timeRange: TimeRange, forceNow?: Date -): Filter | undefined { +) { if (!indexPattern) { // in CI, we sometimes seem to fail here. return; @@ -66,17 +57,13 @@ export function getTime( if (!bounds) { return; } - const filter: Filter = { - range: { [timefield.name]: { format: 'strict_date_optional_time' } }, - }; - - if (bounds.min) { - filter.range[timefield.name].gte = bounds.min.toISOString(); - } - - if (bounds.max) { - filter.range[timefield.name].lte = bounds.max.toISOString(); - } - - return filter; + return esFilters.buildRangeFilter( + timefield, + { + ...(bounds.min && { gte: bounds.min.toISOString() }), + ...(bounds.max && { lte: bounds.max.toISOString() }), + format: 'strict_date_optional_time', + }, + indexPattern + ); } diff --git a/x-pack/legacy/plugins/infra/types/eui.d.ts b/x-pack/legacy/plugins/infra/types/eui.d.ts index 2907830ff882f..7cf0a91e88c1f 100644 --- a/x-pack/legacy/plugins/infra/types/eui.d.ts +++ b/x-pack/legacy/plugins/infra/types/eui.d.ts @@ -68,6 +68,7 @@ declare module '@elastic/eui' { rowProps?: any; cellProps?: any; responsive?: boolean; + itemIdToExpandedRowMap?: any; }; export const EuiInMemoryTable: React.FC; } diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 12fab24d1f8d6..7169014542710 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; +import { getRequestInspectorStats, getResponseInspectorStats } from '../../../../../src/legacy/ui/public/courier'; export { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { start as data } from '../../../../../src/legacy/core_plugins/data/public/legacy'; import { esFilters } from '../../../../../src/plugins/data/public'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; -export { SearchSource } from 'ui/courier'; +export { SearchSource } from '../../../../../src/legacy/ui/public/courier'; export const indexPatternService = data.indexPatterns.indexPatterns; export async function fetchSearchSourceAndRecordWithInspector({ diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts index 2e442c5c61b1e..2bff760ed3711 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts @@ -5,11 +5,12 @@ */ import { searchSourceMock } from '../../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; +import { SearchSourceContract } from '../../../../../../../../../src/legacy/ui/public/courier'; export const savedSearchMock = { id: 'the-saved-search-id', title: 'the-saved-search-title', - searchSource: searchSourceMock, + searchSource: searchSourceMock as SearchSourceContract, columns: [], sort: [], destroy: () => {}, diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 1caa068620618..642b4c5649a13 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -176,7 +176,7 @@ export const Page: FC = () => { const searchSource = currentSavedSearch.searchSource; const query = searchSource.getField('query'); if (query !== undefined) { - const queryLanguage = query.language; + const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; const qryString = query.query; let qry; if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 0e88b291e76fc..455fac9b532d6 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -7,7 +7,7 @@ import { IndexPattern } from 'ui/index_patterns'; import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { esQuery, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { esQuery, Query, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; export interface SearchItems { indexPattern: IIndexPattern; @@ -28,7 +28,7 @@ export function createSearchItems( // a lucene query_string. // Using a blank query will cause match_all:{} to be used // when passed through luceneStringToDsl - let query = { + let query: Query = { query: '', language: 'lucene', }; @@ -45,12 +45,12 @@ export function createSearchItems( if (indexPattern.id === undefined && savedSearch.id !== undefined) { const searchSource = savedSearch.searchSource; - indexPattern = searchSource.getField('index'); + indexPattern = searchSource.getField('index')!; - query = searchSource.getField('query'); + query = searchSource.getField('query')!; const fs = searchSource.getField('filter'); - const filters = fs.length ? fs : []; + const filters = Array.isArray(fs) ? fs : []; const esQueryConfigs = esQuery.getEsQueryConfig(kibanaConfig); combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); diff --git a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts index a614be547abde..aeec71462308e 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts @@ -38,7 +38,7 @@ export function loadNewJobCapabilities( // saved search is being used // load the index pattern from the saved search const savedSearch = await savedSearches.get(savedSearchId); - const indexPattern = savedSearch.searchSource.getField('index'); + const indexPattern = savedSearch.searchSource.getField('index')!; await newJobCapsService.initializeFromIndexPattern(indexPattern); resolve(newJobCapsService.newJobCaps); } else { diff --git a/x-pack/legacy/plugins/rollup/public/search/register.js b/x-pack/legacy/plugins/rollup/public/search/register.js index 917ee872254f5..f7f1c681b63ca 100644 --- a/x-pack/legacy/plugins/rollup/public/search/register.js +++ b/x-pack/legacy/plugins/rollup/public/search/register.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { addSearchStrategy } from 'ui/courier'; +import { addSearchStrategy } from '../../../../../../src/legacy/ui/public/courier'; import { rollupSearchStrategy } from './rollup_search_strategy'; export function initSearch() { diff --git a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js index ab24a37a2ecec..28f08ba1ab952 100644 --- a/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js +++ b/x-pack/legacy/plugins/rollup/public/search/rollup_search_strategy.js @@ -5,7 +5,7 @@ */ import { kfetch } from 'ui/kfetch'; -import { SearchError, getSearchErrorType } from 'ui/courier'; +import { SearchError, getSearchErrorType } from '../../../../../../src/legacy/ui/public/courier'; function serializeFetchParams(searchRequests) { return JSON.stringify(searchRequests.map(searchRequestWithFetchParams => { From a38ff621b6409f7662b5f4555af3315f6a1963e8 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 25 Nov 2019 21:26:51 -0500 Subject: [PATCH 070/128] Add types for Embeddable setup and start contracts (#51654) --- .../kibana/public/dashboard/application.ts | 4 ++-- .../core_plugins/kibana/public/dashboard/plugin.ts | 6 +++--- .../core_plugins/kibana/public/discover/plugin.ts | 9 +++------ src/legacy/ui/public/new_platform/new_platform.ts | 6 +++--- .../public/actions/open_replace_panel_flyout.tsx | 4 ++-- .../public/actions/replace_panel_action.tsx | 4 ++-- .../public/actions/replace_panel_flyout.tsx | 11 +++-------- .../public/embeddable/dashboard_container.tsx | 4 ++-- .../dashboard_embeddable_container/public/plugin.tsx | 6 +++--- src/plugins/embeddable/public/index.ts | 3 +-- src/plugins/embeddable/public/mocks.ts | 9 +++++---- src/plugins/embeddable/public/plugin.ts | 11 +++++++---- src/plugins/embeddable/public/tests/test_plugin.ts | 6 +++--- .../public/np_ready/public/plugin.tsx | 10 +++++++--- .../lens/public/editor_frame_plugin/plugin.tsx | 8 ++++---- x-pack/plugins/advanced_ui_actions/public/plugin.ts | 8 ++++---- 16 files changed, 54 insertions(+), 55 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/application.ts index 57391223fa147..9c50adeeefccb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/application.ts @@ -48,7 +48,7 @@ import { // @ts-ignore import { initDashboardApp } from './legacy_app'; import { DataStart } from '../../../data/public'; -import { EmbeddablePublicPlugin } from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationStart } from '../../../navigation/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; @@ -68,7 +68,7 @@ export interface RenderDeps { chrome: ChromeStart; addBasePath: (path: string) => string; savedQueryService: DataStart['search']['services']['savedQueryService']; - embeddables: ReturnType; + embeddables: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index deb291deb0d5a..609bd717f3c48 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -29,7 +29,7 @@ import { i18n } from '@kbn/i18n'; import { RenderDeps } from './application'; import { DataStart } from '../../../data/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../plugins/data/public'; -import { EmbeddablePublicPlugin } from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationStart } from '../../../navigation/public'; import { DashboardConstants } from './dashboard_constants'; @@ -49,7 +49,7 @@ export interface LegacyAngularInjectedDependencies { export interface DashboardPluginStartDependencies { data: DataStart; npData: NpDataStart; - embeddables: ReturnType; + embeddables: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; } @@ -67,7 +67,7 @@ export class DashboardPlugin implements Plugin { dataStart: DataStart; npDataStart: NpDataStart; savedObjectsClient: SavedObjectsClientContract; - embeddables: ReturnType; + embeddables: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; } | null = null; diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 873c429bf705d..7c2fb4f118915 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -21,10 +21,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/p import { IUiActionsStart } from 'src/plugins/ui_actions/public'; import { registerFeature } from './helpers/register_feature'; import './kibana_services'; -import { - Start as EmbeddableStart, - Setup as EmbeddableSetup, -} from '../../../../../plugins/embeddable/public'; +import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; /** * These are the interfaces with your public contracts. You should export these @@ -35,11 +32,11 @@ export type DiscoverSetup = void; export type DiscoverStart = void; interface DiscoverSetupPlugins { uiActions: IUiActionsStart; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; } interface DiscoverStartPlugins { uiActions: IUiActionsStart; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; } export class DiscoverPlugin implements Plugin { diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 36bfbcc7d5d46..c0b2d6d913257 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -19,7 +19,7 @@ import { IScope } from 'angular'; import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; -import { Start as EmbeddableStart, Setup as EmbeddableSetup } from 'src/plugins/embeddable/public'; +import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { LegacyCoreSetup, LegacyCoreStart, App } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; @@ -35,7 +35,7 @@ import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/pu export interface PluginsSetup { data: ReturnType; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; expressions: ReturnType; home: HomePublicPluginSetup; inspector: InspectorSetup; @@ -47,7 +47,7 @@ export interface PluginsSetup { export interface PluginsStart { data: ReturnType; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; eui_utils: EuiUtilsStart; expressions: ReturnType; home: HomePublicPluginStart; diff --git a/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx index b30733760bbdf..f15d538703e21 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/open_replace_panel_flyout.tsx @@ -24,7 +24,7 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput, - Start as EmbeddableStart, + IEmbeddableStart, IContainer, } from '../embeddable_plugin'; @@ -34,7 +34,7 @@ export async function openReplacePanelFlyout(options: { savedObjectFinder: React.ComponentType; notifications: CoreStart['notifications']; panelToRemove: IEmbeddable; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; }) { const { embeddable, diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx index f6d2fcbcd57fd..78ce6bdc4c58f 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; -import { IEmbeddable, ViewMode, Start as EmbeddableStart } from '../embeddable_plugin'; +import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; import { IAction, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; @@ -43,7 +43,7 @@ export class ReplacePanelAction implements IAction { private core: CoreStart, private savedobjectfinder: React.ComponentType, private notifications: CoreStart['notifications'], - private getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'] + private getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'] ) {} public getDisplayName({ embeddable }: ActionContext) { diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx index 36efd0bcba676..36313353e3c33 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx @@ -20,15 +20,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import { GetEmbeddableFactories } from 'src/plugins/embeddable/public'; import { DashboardPanelState } from '../embeddable'; import { NotificationsStart, Toast } from '../../../../core/public'; -import { - IContainer, - IEmbeddable, - EmbeddableInput, - EmbeddableOutput, - Start as EmbeddableStart, -} from '../embeddable_plugin'; +import { IContainer, IEmbeddable, EmbeddableInput, EmbeddableOutput } from '../embeddable_plugin'; interface Props { container: IContainer; @@ -36,7 +31,7 @@ interface Props { onClose: () => void; notifications: NotificationsStart; panelToRemove: IEmbeddable; - getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: GetEmbeddableFactories; } export class ReplacePanelFlyout extends React.Component { diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx index 6cefd11c912f1..684aa93779bc1 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx @@ -30,7 +30,7 @@ import { ViewMode, EmbeddableFactory, IEmbeddable, - Start as EmbeddableStartContract, + IEmbeddableStart, } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; import { createPanelState } from './panel'; @@ -77,7 +77,7 @@ export interface DashboardContainerOptions { application: CoreStart['application']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; - embeddable: EmbeddableStartContract; + embeddable: IEmbeddableStart; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; diff --git a/src/plugins/dashboard_embeddable_container/public/plugin.tsx b/src/plugins/dashboard_embeddable_container/public/plugin.tsx index dbb5a06da9cd9..79cc9b6980545 100644 --- a/src/plugins/dashboard_embeddable_container/public/plugin.tsx +++ b/src/plugins/dashboard_embeddable_container/public/plugin.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { IUiActionsSetup, IUiActionsStart } from '../../../plugins/ui_actions/public'; -import { CONTEXT_MENU_TRIGGER, Plugin as EmbeddablePlugin } from './embeddable_plugin'; +import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin'; import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; @@ -34,12 +34,12 @@ import { } from '../../../plugins/kibana_react/public'; interface SetupDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableSetup; uiActions: IUiActionsSetup; } interface StartDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableStart; inspector: InspectorStartContract; uiActions: IUiActionsStart; } diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 33855b07df7a1..ea2bd910b0624 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -61,5 +61,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } -export { EmbeddablePublicPlugin as Plugin }; -export * from './plugin'; +export { IEmbeddableSetup, IEmbeddableStart } from './plugin'; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index ef1517bb7f1d5..fd299bc626fb9 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -17,14 +17,15 @@ * under the License. */ -import { Plugin } from '.'; +import { IEmbeddableStart, IEmbeddableSetup } from '.'; +import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; -export type Setup = jest.Mocked>; -export type Start = jest.Mocked>; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { @@ -43,7 +44,7 @@ const createStartContract = (): Start => { }; const createInstance = () => { - const plugin = new Plugin({} as any); + const plugin = new EmbeddablePublicPlugin({} as any); const setup = plugin.setup(coreMock.createSetup(), { uiActions: uiActionsPluginMock.createSetupContract(), }); diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index 458c8bfeb8762..df1f4e5080031 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -27,7 +27,13 @@ export interface IEmbeddableSetupDependencies { uiActions: IUiActionsSetup; } -export class EmbeddablePublicPlugin implements Plugin { +export interface IEmbeddableSetup { + registerEmbeddableFactory: EmbeddableApi['registerEmbeddableFactory']; +} + +export type IEmbeddableStart = EmbeddableApi; + +export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private api!: EmbeddableApi; @@ -52,6 +58,3 @@ export class EmbeddablePublicPlugin implements Plugin { public stop() {} } - -export type Setup = ReturnType; -export type Start = ReturnType; diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index 5b50bddefcdb7..6d1e15137480a 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -21,14 +21,14 @@ import { CoreSetup, CoreStart } from 'src/core/public'; // eslint-disable-next-line import { uiActionsTestPlugin } from 'src/plugins/ui_actions/public/tests'; import { IUiActionsApi } from 'src/plugins/ui_actions/public'; -import { EmbeddablePublicPlugin } from '../plugin'; +import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin'; export interface TestPluginReturn { plugin: EmbeddablePublicPlugin; coreSetup: CoreSetup; coreStart: CoreStart; - setup: ReturnType; - doStart: (anotherCoreStart?: CoreStart) => ReturnType; + setup: IEmbeddableSetup; + doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart; uiActions: IUiActionsApi; } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index f03b3c4a1e0a5..6b82a67b9fcda 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -27,7 +27,7 @@ import { Setup as InspectorSetupContract, } from '../../../../../../../src/plugins/inspector/public'; -import { Plugin as EmbeddablePlugin, CONTEXT_MENU_TRIGGER } from './embeddable_api'; +import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; const REACT_ROOT_ID = 'embeddableExplorerRoot'; @@ -38,9 +38,13 @@ import { ContactCardEmbeddableFactory, } from './embeddable_api'; import { App } from './app'; +import { + IEmbeddableStart, + IEmbeddableSetup, +} from '.../../../../../../../src/plugins/embeddable/public'; export interface SetupDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableSetup; inspector: InspectorSetupContract; __LEGACY: { SavedObjectFinder: React.ComponentType; @@ -49,7 +53,7 @@ export interface SetupDependencies { } interface StartDependencies { - embeddable: ReturnType; + embeddable: IEmbeddableStart; uiActions: IUiActionsStart; inspector: InspectorStartContract; __LEGACY: { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 354a5186db4c1..f7399255b2001 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -15,8 +15,8 @@ import { ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; import { - Setup as EmbeddableSetup, - Start as EmbeddableStart, + IEmbeddableSetup, + IEmbeddableStart, } from '../../../../../../src/plugins/embeddable/public'; import { setup as dataSetup, @@ -36,13 +36,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: typeof dataSetup; - embeddable: EmbeddableSetup; + embeddable: IEmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: typeof dataStart; - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; expressions: ExpressionsStart; chrome: Chrome; } diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index e2d1892b1355e..cc4a7c90de513 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -15,8 +15,8 @@ import { IUiActionsStart, IUiActionsSetup } from '../../../../src/plugins/ui_act import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, - Setup as EmbeddableSetup, - Start as EmbeddableStart, + IEmbeddableSetup, + IEmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { CustomTimeRangeAction } from './custom_time_range_action'; @@ -24,12 +24,12 @@ import { CustomTimeRangeBadge } from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; interface SetupDependencies { - embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. + embeddable: IEmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: IUiActionsSetup; } interface StartDependencies { - embeddable: EmbeddableStart; + embeddable: IEmbeddableStart; uiActions: IUiActionsStart; } From d158fd8881ad98b18a4f5233ffeed85a663402fa Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 25 Nov 2019 19:27:08 -0700 Subject: [PATCH 071/128] [Maps] only provide visiblity check when vector layer has joins (#51388) * [Maps] only provide visiblity check when vector layer has joins * clean up * properly set line filter expression * use ternary statements instead of if * review feedback --- .../legacy/plugins/maps/common/constants.js | 2 +- .../layers/util/mb_filter_expressions.js | 62 +++++++++++++++ .../maps/public/layers/vector_layer.js | 77 ++++++++----------- x-pack/test/functional/apps/maps/joins.js | 4 +- .../functional/apps/maps/mapbox_styles.js | 6 +- 5 files changed, 102 insertions(+), 49 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 691c679e5290b..3b2f887e13c87 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -68,7 +68,7 @@ export const ZOOM_PRECISION = 2; export const ES_SIZE_LIMIT = 10000; export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__'; -export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn__isvisible__'; +export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js new file mode 100644 index 0000000000000..393c290d69668 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/mb_filter_expressions.js @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; + +const VISIBILITY_FILTER_CLAUSE = ['all', + [ + '==', + ['get', FEATURE_VISIBLE_PROPERTY_NAME], + true + ] +]; + +const CLOSED_SHAPE_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON] +]; + +const VISIBLE_CLOSED_SHAPE_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + CLOSED_SHAPE_MB_FILTER, +]; + +const ALL_SHAPE_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] +]; + +const VISIBLE_ALL_SHAPE_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + ALL_SHAPE_MB_FILTER, +]; + +const POINT_MB_FILTER = [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT] +]; + +const VISIBLE_POINT_MB_FILTER = [ + ...VISIBILITY_FILTER_CLAUSE, + POINT_MB_FILTER, +]; + +export function getFillFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_CLOSED_SHAPE_MB_FILTER : CLOSED_SHAPE_MB_FILTER; +} + +export function getLineFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_ALL_SHAPE_MB_FILTER : ALL_SHAPE_MB_FILTER; +} + +export function getPointFilterExpression(hasJoins) { + return hasJoins ? VISIBLE_POINT_MB_FILTER : POINT_MB_FILTER; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index e6b07b983d898..9b553803606ed 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -10,7 +10,6 @@ import { AbstractLayer } from './layer'; import { VectorStyle } from './styles/vector/vector_style'; import { InnerJoin } from './joins/inner_join'; import { - GEO_JSON_TYPE, FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, @@ -24,41 +23,11 @@ import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; import { assignFeatureIds } from './util/assign_feature_ids'; - -const VISIBILITY_FILTER_CLAUSE = ['all', - [ - '==', - ['get', FEATURE_VISIBLE_PROPERTY_NAME], - true - ] -]; - -const FILL_LAYER_MB_FILTER = [ - ...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON] - ] -]; - -const LINE_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING] - ] -]; - -const POINT_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, - [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT] - ] -]; +import { + getFillFilterExpression, + getLineFilterExpression, + getPointFilterExpression, +} from './util/mb_filter_expressions'; export class VectorLayer extends AbstractLayer { @@ -107,6 +76,10 @@ export class VectorLayer extends AbstractLayer { }); } + _hasJoins() { + return this.getValidJoins().length > 0; + } + isDataLoaded() { const sourceDataRequest = this.getSourceDataRequest(); if (!sourceDataRequest || !sourceDataRequest.hasData()) { @@ -495,12 +468,14 @@ export class VectorLayer extends AbstractLayer { } const sourceResult = await this._syncSource(syncContext); - if (!sourceResult.featureCollection || !sourceResult.featureCollection.features.length) { + if ( + !sourceResult.featureCollection || + !sourceResult.featureCollection.features.length || + !this._hasJoins()) { return; } const joinStates = await this._syncJoins(syncContext); - await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); } @@ -571,7 +546,11 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(pointLayerId, POINT_LAYER_MB_FILTER); + } + + const filterExpr = getPointFilterExpression(this._hasJoins()); + if (filterExpr !== mbMap.getFilter(pointLayerId)) { + mbMap.setFilter(pointLayerId, filterExpr); } this._style.setMBPaintPropertiesForPoints({ @@ -592,7 +571,11 @@ export class VectorLayer extends AbstractLayer { type: 'symbol', source: sourceId, }); - mbMap.setFilter(symbolLayerId, POINT_LAYER_MB_FILTER); + } + + const filterExpr = getPointFilterExpression(this._hasJoins()); + if (filterExpr !== mbMap.getFilter(symbolLayerId)) { + mbMap.setFilter(symbolLayerId, filterExpr); } this._style.setMBSymbolPropertiesForPoints({ @@ -606,6 +589,7 @@ export class VectorLayer extends AbstractLayer { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); + const hasJoins = this._hasJoins(); if (!mbMap.getLayer(fillLayerId)) { mbMap.addLayer({ id: fillLayerId, @@ -613,7 +597,6 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(fillLayerId, FILL_LAYER_MB_FILTER); } if (!mbMap.getLayer(lineLayerId)) { mbMap.addLayer({ @@ -622,7 +605,6 @@ export class VectorLayer extends AbstractLayer { source: sourceId, paint: {} }); - mbMap.setFilter(lineLayerId, LINE_LAYER_MB_FILTER); } this._style.setMBPaintProperties({ alpha: this.getAlpha(), @@ -632,9 +614,18 @@ export class VectorLayer extends AbstractLayer { }); this.syncVisibilityWithMb(mbMap, fillLayerId); + mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + const fillFilterExpr = getFillFilterExpression(hasJoins); + if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) { + mbMap.setFilter(fillLayerId, fillFilterExpr); + } + this.syncVisibilityWithMb(mbMap, lineLayerId); mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); - mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + const lineFilterExpr = getLineFilterExpression(hasJoins); + if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) { + mbMap.setFilter(lineLayerId, lineFilterExpr); + } } _syncStylePropertiesWithMb(mbMap) { diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 1634bea47a69f..30b957fdf45f4 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -102,7 +102,7 @@ export default function ({ getPageObjects, getService }) { const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; const visibilitiesOfFeatures = vectorSource.data.features.map(feature => { - return feature.properties.__kbn__isvisible__; + return feature.properties.__kbn_isvisibleduetojoin__; }); expect(visibilitiesOfFeatures).to.eql([false, true, true, true]); @@ -166,7 +166,7 @@ export default function ({ getPageObjects, getService }) { const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; const visibilitiesOfFeatures = vectorSource.data.features.map(feature => { - return feature.properties.__kbn__isvisible__; + return feature.properties.__kbn_isvisibleduetojoin__; }); expect(visibilitiesOfFeatures).to.eql([false, true, false, false]); diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index 49519b530337e..bfa4be2b067af 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -15,7 +15,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ @@ -89,7 +89,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ @@ -160,7 +160,7 @@ export const MAPBOX_STYLES = { 'all', [ '==', - ['get', '__kbn__isvisible__'], + ['get', '__kbn_isvisibleduetojoin__'], true ], [ From 320fa5a550953f672d3293bdc2532a6981d4c87d Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 25 Nov 2019 20:58:35 -0700 Subject: [PATCH 072/128] Default search params for ignore_unavailable and rest_total_hits_as_int (#50953) * Default search params for ignore_unavailable and rest_total_hits_as_int * Update code with options arg * Fix filter matches index for filters with partial meta * Fix tests --- src/plugins/data/public/search/i_search.ts | 4 +-- .../public/search/sync_search_strategy.ts | 2 +- .../data/server/search/create_api.test.ts | 2 +- src/plugins/data/server/search/create_api.ts | 2 +- .../es_search/es_search_strategy.test.ts | 28 +++++++++++++++++-- .../search/es_search/es_search_strategy.ts | 17 +++++------ src/plugins/data/server/search/i_search.ts | 8 +++++- src/plugins/data/server/search/routes.test.ts | 4 +-- src/plugins/data/server/search/routes.ts | 2 +- .../data/server/search/search_service.ts | 2 +- .../plugins/demo_search/server/constants.ts | 20 ------------- 11 files changed, 51 insertions(+), 40 deletions(-) delete mode 100644 test/plugin_functional/plugins/demo_search/server/constants.ts diff --git a/src/plugins/data/public/search/i_search.ts b/src/plugins/data/public/search/i_search.ts index 0e256b960ffa3..a39ef3e3e7571 100644 --- a/src/plugins/data/public/search/i_search.ts +++ b/src/plugins/data/public/search/i_search.ts @@ -49,11 +49,11 @@ export interface IResponseTypesMap { export type ISearchGeneric = ( request: IRequestTypesMap[T], - options: ISearchOptions, + options?: ISearchOptions, strategy?: T ) => Observable; export type ISearch = ( request: IRequestTypesMap[T], - options: ISearchOptions + options?: ISearchOptions ) => Observable; diff --git a/src/plugins/data/public/search/sync_search_strategy.ts b/src/plugins/data/public/search/sync_search_strategy.ts index c412bbb3b104a..3885a97a98571 100644 --- a/src/plugins/data/public/search/sync_search_strategy.ts +++ b/src/plugins/data/public/search/sync_search_strategy.ts @@ -34,7 +34,7 @@ export const syncSearchStrategyProvider: TSearchStrategyProvider { const search: ISearch = ( request: ISyncSearchRequest, - options: ISearchOptions + options: ISearchOptions = {} ) => { const response: Promise = context.core.http.fetch( `/internal/search/${request.serverStrategy}`, diff --git a/src/plugins/data/server/search/create_api.test.ts b/src/plugins/data/server/search/create_api.test.ts index 32570a05031f6..cc13269e1aa21 100644 --- a/src/plugins/data/server/search/create_api.test.ts +++ b/src/plugins/data/server/search/create_api.test.ts @@ -55,7 +55,7 @@ describe('createApi', () => { }); it('should throw if no provider is found for the given name', () => { - expect(api.search({}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( + expect(api.search({}, {}, 'noneByThisName')).rejects.toThrowErrorMatchingInlineSnapshot( `"No strategy found for noneByThisName"` ); }); diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts index 4c13dd9e1137c..2a874869526d7 100644 --- a/src/plugins/data/server/search/create_api.ts +++ b/src/plugins/data/server/search/create_api.ts @@ -30,7 +30,7 @@ export function createApi({ caller: APICaller; }) { const api: IRouteHandlerSearchContext = { - search: async (request, strategyName) => { + search: async (request, options, strategyName) => { const name = strategyName ? strategyName : DEFAULT_SEARCH_STRATEGY; const strategyProvider = searchStrategies[name]; if (!strategyProvider) { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 619a28df839bd..7b725a47aa13b 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -66,7 +66,7 @@ describe('ES search strategy', () => { expect(spy).toBeCalled(); }); - it('calls the API caller with the params', () => { + it('calls the API caller with the params with defaults', () => { const params = { index: 'logstash-*' }; const esSearch = esSearchStrategyProvider( { @@ -80,7 +80,31 @@ describe('ES search strategy', () => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); - expect(mockApiCaller.mock.calls[0][1]).toEqual(params); + expect(mockApiCaller.mock.calls[0][1]).toEqual({ + ...params, + ignoreUnavailable: true, + restTotalHitsAsInt: true, + }); + }); + + it('calls the API caller with overridden defaults', () => { + const params = { index: 'logstash-*', ignoreUnavailable: false }; + const esSearch = esSearchStrategyProvider( + { + core: mockCoreSetup, + }, + mockApiCaller, + mockSearch + ); + + esSearch.search({ params }); + + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toBe('search'); + expect(mockApiCaller.mock.calls[0][1]).toEqual({ + ...params, + restTotalHitsAsInt: true, + }); }); it('returns total, loaded, and raw response', async () => { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 31f4fc15a0989..c5fc1d9d3a11c 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -18,7 +18,7 @@ */ import { APICaller } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../../common/search'; +import { ES_SEARCH_STRATEGY } from '../../../common/search'; import { ISearchStrategy, TSearchStrategyProvider } from '../i_search_strategy'; import { ISearchContext } from '..'; @@ -27,16 +27,17 @@ export const esSearchStrategyProvider: TSearchStrategyProvider => { return { - search: async (request: IEsSearchRequest) => { + search: async (request, options) => { + const params = { + ignoreUnavailable: true, // Don't fail if the index/indices don't exist + restTotalHitsAsInt: true, // Get the number of hits as an int rather than a range + ...request.params, + }; if (request.debug) { // eslint-disable-next-line - console.log(JSON.stringify(request, null, 2)); + console.log(JSON.stringify(params, null, 2)); } - const esSearchResponse = (await caller('search', { - ...request.params, - // TODO: could do something like this here? - // ...getCurrentSearchParams(context), - })) as SearchResponse; + const esSearchResponse = (await caller('search', params, options)) as SearchResponse; // The above query will either complete or timeout and throw an error. // There is no progress indication on this api. diff --git a/src/plugins/data/server/search/i_search.ts b/src/plugins/data/server/search/i_search.ts index fabcb98ceea72..0a35734574153 100644 --- a/src/plugins/data/server/search/i_search.ts +++ b/src/plugins/data/server/search/i_search.ts @@ -22,6 +22,10 @@ import { TStrategyTypes } from './strategy_types'; import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../common/search/es_search'; import { IEsSearchRequest } from './es_search'; +export interface ISearchOptions { + signal?: AbortSignal; +} + export interface IRequestTypesMap { [ES_SEARCH_STRATEGY]: IEsSearchRequest; [key: string]: IKibanaSearchRequest; @@ -34,9 +38,11 @@ export interface IResponseTypesMap { export type ISearchGeneric = ( request: IRequestTypesMap[T], + options?: ISearchOptions, strategy?: T ) => Promise; export type ISearch = ( - request: IRequestTypesMap[T] + request: IRequestTypesMap[T], + options?: ISearchOptions ) => Promise; diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index ebdcf48f608b9..a2394d88f3931 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -60,7 +60,7 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' }); }); @@ -92,7 +92,7 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); - expect(mockSearch.mock.calls[0][1]).toBe(mockParams.strategy); + expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); expect(mockResponse.internalError).toBeCalled(); expect(mockResponse.internalError.mock.calls[0][0]).toEqual({ body: 'oh no' }); }); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 6cb6c28c76014..eaa72548e08ee 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -36,7 +36,7 @@ export function registerSearchRoute(router: IRouter): void { const searchRequest = request.body; const strategy = request.params.strategy; try { - const response = await context.search!.search(searchRequest, strategy); + const response = await context.search!.search(searchRequest, {}, strategy); return res.ok({ body: response }); } catch (err) { return res.internalError({ body: err }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 4edb51300dfaf..3409a72326121 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -77,7 +77,7 @@ export class SearchService implements Plugin { caller, searchStrategies: this.searchStrategies, }); - return searchAPI.search(request, strategyName); + return searchAPI.search(request, {}, strategyName); }, }, }; diff --git a/test/plugin_functional/plugins/demo_search/server/constants.ts b/test/plugin_functional/plugins/demo_search/server/constants.ts deleted file mode 100644 index 11c258a21d5a8..0000000000000 --- a/test/plugin_functional/plugins/demo_search/server/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const FAKE_PROGRESS_STRATEGY = 'FAKE_PROGRESS_STRATEGY'; From 1e6f9d54fe1155f0766515ba2ece02a7828b8a71 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 26 Nov 2019 04:04:31 +0000 Subject: [PATCH 073/128] chore(NA): ability to add manual exceptions for the clean dll logic on the build (#51642) --- .../clean_client_modules_on_dll_task.js | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 5c0462ce86fa9..b0e38b6481457 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -66,10 +66,38 @@ export const CleanClientModulesOnDLLTask = { // side code entries that were provided const serverDependencies = await getDependencies(baseDir, serverEntries); + // This fulfill a particular exceptional case where + // we need to keep loading a file from a node_module + // only used in the front-end like we do when using the file-loader + // in https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js + // + // manual list of exception modules + const manualExceptionModules = [ + 'mapbox-gl' + ]; + + // consider the top modules as exceptions as the entry points + // to look for other exceptions dependent on that one + const manualExceptionEntries = [ + ...manualExceptionModules.map(module => `${baseDir}/node_modules/${module}`) + ]; + + // dependencies for declared exception modules + const manualExceptionModulesDependencies = await getDependencies(baseDir, [ + ...manualExceptionEntries + ]); + + // final list of manual exceptions to add + const manualExceptions = [ + ...manualExceptionModules, + ...manualExceptionModulesDependencies + ]; + // Consider this as our whiteList for the modules we can't delete const whiteListedModules = [ ...serverDependencies, - ...kbnWebpackLoaders + ...kbnWebpackLoaders, + ...manualExceptions ]; // Resolve the client vendors dll manifest path From c4141fa62bcd7b5a7aa60dd6e7c088237e4a2c10 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 26 Nov 2019 00:44:26 -0700 Subject: [PATCH 074/128] skip flaky suite (#45321) --- x-pack/test/functional/apps/graph/graph.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index cb6f0b6028a2d..f640a34b36ddf 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -13,7 +13,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - describe('graph', function() { + // FLAKY: https://github.com/elastic/kibana/issues/45321 + describe.skip('graph', function() { before(async () => { await browser.setWindowSize(1600, 1000); log.debug('load graph/secrepo data'); From e753c35b9d37b32717aaa5fc0555e506253c40d2 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 26 Nov 2019 09:16:05 +0100 Subject: [PATCH 075/128] performs logout using the API (#51596) --- .../plugins/siem/cypress/integration/lib/logout/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts index 132242606d88c..7a6c7f71bc98c 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/logout/index.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LOGOUT } from '../urls'; - export const logout = (): null => { - cy.visit(`${Cypress.config().baseUrl}${LOGOUT}`); + cy.request({ + method: 'GET', + url: `${Cypress.config().baseUrl}/logout`, + }).then(response => { + expect(response.status).to.eq(200); + }); return null; }; From 7fef618ea6a1d370240ed10359f6321f56d11279 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 26 Nov 2019 10:21:49 +0100 Subject: [PATCH 076/128] Add compatibility wrapper for Boom errors thrown from route handler (#51157) * add wrapErrors method to router * add RouteRegistrar type * update generated doc * add migration example * rename wrapErrors to handleLegacyErrors --- .../kibana-plugin-server.irouter.delete.md | 2 +- .../kibana-plugin-server.irouter.get.md | 2 +- ...lugin-server.irouter.handlelegacyerrors.md | 13 +++ .../server/kibana-plugin-server.irouter.md | 9 ++- .../kibana-plugin-server.irouter.post.md | 2 +- .../kibana-plugin-server.irouter.put.md | 2 +- .../core/server/kibana-plugin-server.md | 1 + .../kibana-plugin-server.routeregistrar.md | 13 +++ src/core/MIGRATION_EXAMPLES.md | 29 +++++++ src/core/server/http/http_service.mock.ts | 1 + src/core/server/http/index.ts | 1 + .../http/integration_tests/router.test.ts | 47 +++++++++++ .../server/http/router/error_wrapper.test.ts | 80 +++++++++++++++++++ src/core/server/http/router/error_wrapper.ts | 48 +++++++++++ src/core/server/http/router/index.ts | 2 +- src/core/server/http/router/router.ts | 42 ++++++---- src/core/server/index.ts | 1 + src/core/server/server.api.md | 12 ++- 18 files changed, 279 insertions(+), 28 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeregistrar.md create mode 100644 src/core/server/http/router/error_wrapper.test.ts create mode 100644 src/core/server/http/router/error_wrapper.ts diff --git a/docs/development/core/server/kibana-plugin-server.irouter.delete.md b/docs/development/core/server/kibana-plugin-server.irouter.delete.md index 9124b4a1b21c4..5202e0cfd5ebb 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.delete.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.delete.md @@ -9,5 +9,5 @@ Register a route handler for `DELETE` request. Signature: ```typescript -delete:

    (route: RouteConfig, handler: RequestHandler) => void; +delete: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.get.md b/docs/development/core/server/kibana-plugin-server.irouter.get.md index 0291906c6fc6b..32552a49cb999 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.get.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.get.md @@ -9,5 +9,5 @@ Register a route handler for `GET` request. Signature: ```typescript -get:

    (route: RouteConfig, handler: RequestHandler) => void; +get: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md new file mode 100644 index 0000000000000..2367420068064 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) + +## IRouter.handleLegacyErrors property + +Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + +Signature: + +```typescript +handleLegacyErrors:

    (handler: RequestHandler) => RequestHandler; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md index bbffe1e42f229..b5d3c893d745d 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.md @@ -16,9 +16,10 @@ export interface IRouter | Property | Type | Description | | --- | --- | --- | -| [delete](./kibana-plugin-server.irouter.delete.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for DELETE request. | -| [get](./kibana-plugin-server.irouter.get.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for GET request. | -| [post](./kibana-plugin-server.irouter.post.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for POST request. | -| [put](./kibana-plugin-server.irouter.put.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for PUT request. | +| [delete](./kibana-plugin-server.irouter.delete.md) | RouteRegistrar | Register a route handler for DELETE request. | +| [get](./kibana-plugin-server.irouter.get.md) | RouteRegistrar | Register a route handler for GET request. | +| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | +| [post](./kibana-plugin-server.irouter.post.md) | RouteRegistrar | Register a route handler for POST request. | +| [put](./kibana-plugin-server.irouter.put.md) | RouteRegistrar | Register a route handler for PUT request. | | [routerPath](./kibana-plugin-server.irouter.routerpath.md) | string | Resulted path | diff --git a/docs/development/core/server/kibana-plugin-server.irouter.post.md b/docs/development/core/server/kibana-plugin-server.irouter.post.md index e97a32e433ce9..cd655c9ce0dc8 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.post.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.post.md @@ -9,5 +9,5 @@ Register a route handler for `POST` request. Signature: ```typescript -post:

    (route: RouteConfig, handler: RequestHandler) => void; +post: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.put.md b/docs/development/core/server/kibana-plugin-server.irouter.put.md index 25db91e389939..e553d4b79dd2b 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.put.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.put.md @@ -9,5 +9,5 @@ Register a route handler for `PUT` request. Signature: ```typescript -put:

    (route: RouteConfig, handler: RequestHandler) => void; +put: RouteRegistrar; ``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index c6ab8502acbd2..360675b3490c2 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -170,6 +170,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | +| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Handler to declare a route. | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | diff --git a/docs/development/core/server/kibana-plugin-server.routeregistrar.md b/docs/development/core/server/kibana-plugin-server.routeregistrar.md new file mode 100644 index 0000000000000..535927dc73743 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeregistrar.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) + +## RouteRegistrar type + +Handler to declare a route. + +Signature: + +```typescript +export declare type RouteRegistrar =

    (route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 9eed3a59acaa6..ccf14879baa37 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -298,6 +298,35 @@ class Plugin { } } ``` +If your plugin still relies on throwing Boom errors from routes, you can use the `router.handleLegacyErrors` +as a temporary solution until error migration is complete: +```ts +// legacy/plugins/demoplugin/server/plugin.ts +import { schema } from '@kbn/config-schema'; +import { CoreSetup } from 'src/core/server'; + +export interface DemoPluginsSetup {}; + +class Plugin { + public setup(core: CoreSetup, pluginSetup: DemoPluginSetup) { + const router = core.http.createRouter(); + router.post( + { + path: '/api/demoplugin/search', + validate: { + body: schema.object({ + field1: schema.string(), + }), + } + }, + router.wrapErrors((context, req, res) => { + throw Boom.notFound('not there'); // will be converted into proper New Platform error + }) + ) + } +} +``` + #### 4. New Platform plugin As the final step we delete the shim and move all our code into a New Platform diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 00c9aedc42cfb..e9a2571382edc 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -45,6 +45,7 @@ const createRouterMock = (): jest.Mocked => ({ put: jest.fn(), delete: jest.fn(), getRoutes: jest.fn(), + handleLegacyErrors: jest.fn().mockImplementation(handler => handler), }); const createSetupContractMock = () => { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index ff1ff3acfae3d..2fa67750f6406 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -45,6 +45,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 70d7ae00f917e..481d8e1bbf49b 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -164,6 +164,53 @@ describe('Handler', () => { }); }); +describe('handleLegacyErrors', () => { + it('properly convert Boom errors', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false }, + router.handleLegacyErrors((context, req, res) => { + throw Boom.notFound(); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(404); + + expect(result.body.message).toBe('Not Found'); + }); + + it('returns default error when non-Boom errors are thrown', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { + path: '/', + validate: false, + }, + router.handleLegacyErrors((context, req, res) => { + throw new Error('Unexpected'); + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred.', + statusCode: 500, + }); + }); +}); + describe('Response factory', () => { describe('Success', () => { it('supports answering with json object', async () => { diff --git a/src/core/server/http/router/error_wrapper.test.ts b/src/core/server/http/router/error_wrapper.test.ts new file mode 100644 index 0000000000000..aa20b49dc9c91 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.test.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { KibanaResponse, KibanaResponseFactory, kibanaResponseFactory } from './response'; +import { wrapErrors } from './error_wrapper'; +import { KibanaRequest, RequestHandler, RequestHandlerContext } from 'kibana/server'; + +const createHandler = (handler: () => any): RequestHandler => () => { + return handler(); +}; + +describe('wrapErrors', () => { + let context: RequestHandlerContext; + let request: KibanaRequest; + let response: KibanaResponseFactory; + + beforeEach(() => { + context = {} as any; + request = {} as any; + response = kibanaResponseFactory; + }); + + it('should pass-though call parameters to the handler', async () => { + const handler = jest.fn(); + const wrapped = wrapErrors(handler); + await wrapped(context, request, response); + expect(handler).toHaveBeenCalledWith(context, request, response); + }); + + it('should pass-though result from the handler', async () => { + const handler = createHandler(() => { + return 'handler-response'; + }); + const wrapped = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBe('handler-response'); + }); + + it('should intercept and convert thrown Boom errors', async () => { + const handler = createHandler(() => { + throw Boom.notFound('not there'); + }); + const wrapped = wrapErrors(handler); + const result = await wrapped(context, request, response); + expect(result).toBeInstanceOf(KibanaResponse); + expect(result.status).toBe(404); + expect(result.payload).toEqual({ + error: 'Not Found', + message: 'not there', + statusCode: 404, + }); + }); + + it('should re-throw non-Boom errors', async () => { + const handler = createHandler(() => { + throw new Error('something went bad'); + }); + const wrapped = wrapErrors(handler); + await expect(wrapped(context, request, response)).rejects.toMatchInlineSnapshot( + `[Error: something went bad]` + ); + }); +}); diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts new file mode 100644 index 0000000000000..706a9fe3b8887 --- /dev/null +++ b/src/core/server/http/router/error_wrapper.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { ObjectType, TypeOf } from '@kbn/config-schema'; +import { KibanaRequest } from './request'; +import { KibanaResponseFactory } from './response'; +import { RequestHandler } from './router'; +import { RequestHandlerContext } from '../../../server'; + +export const wrapErrors =

    ( + handler: RequestHandler +): RequestHandler => { + return async ( + context: RequestHandlerContext, + request: KibanaRequest, TypeOf, TypeOf>, + response: KibanaResponseFactory + ) => { + try { + return await handler(context, request, response); + } catch (e) { + if (Boom.isBoom(e)) { + return response.customError({ + body: e.output.payload, + statusCode: e.output.statusCode, + headers: e.output.headers, + }); + } + throw e; + } + }; +}; diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 56ed9ca11edc1..f07ad3cfe85c0 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -18,7 +18,7 @@ */ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; -export { Router, RequestHandler, IRouter } from './router'; +export { Router, RequestHandler, IRouter, RouteRegistrar } from './router'; export { KibanaRequest, KibanaRequestRoute, diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 6b7e2e3ad14cd..a13eae51a19a6 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -27,6 +27,7 @@ import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from '. import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; import { HapiResponseAdapter } from './response_adapter'; import { RequestHandlerContext } from '../../../server'; +import { wrapErrors } from './error_wrapper'; interface RouterRoute { method: RouteMethod; @@ -35,6 +36,15 @@ interface RouterRoute { handler: (req: Request, responseToolkit: ResponseToolkit) => Promise>; } +/** + * Handler to declare a route. + * @public + */ +export type RouteRegistrar =

    ( + route: RouteConfig, + handler: RequestHandler +) => void; + /** * Registers route handlers for specified resource path and method. * See {@link RouteConfig} and {@link RequestHandler} for more information about arguments to route registrations. @@ -52,40 +62,36 @@ export interface IRouter { * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - get:

    ( - route: RouteConfig, - handler: RequestHandler - ) => void; + get: RouteRegistrar; /** * Register a route handler for `POST` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - post:

    ( - route: RouteConfig, - handler: RequestHandler - ) => void; + post: RouteRegistrar; /** * Register a route handler for `PUT` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - put:

    ( - route: RouteConfig, - handler: RequestHandler - ) => void; + put: RouteRegistrar; /** * Register a route handler for `DELETE` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - delete:

    ( - route: RouteConfig, + delete: RouteRegistrar; + + /** + * Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + * @param handler {@link RequestHandler} - a route handler to wrap + */ + handleLegacyErrors:

    ( handler: RequestHandler - ) => void; + ) => RequestHandler; /** * Returns all routes registered with the this router. @@ -188,6 +194,12 @@ export class Router implements IRouter { return [...this.routes]; } + public handleLegacyErrors

    ( + handler: RequestHandler + ): RequestHandler { + return wrapErrors(handler); + } + private async handle

    ({ routeSchemas, request, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 987e4e64f9d5b..31dec2c9b96ff 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -114,6 +114,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteRegistrar, SessionStorage, SessionStorageCookieOptions, SessionStorageFactory, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 066f79bfd38f3..d6cfa54397565 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -714,14 +714,15 @@ export interface IndexSettingsDeprecationInfo { // @public export interface IRouter { - delete:

    (route: RouteConfig, handler: RequestHandler) => void; - get:

    (route: RouteConfig, handler: RequestHandler) => void; + delete: RouteRegistrar; + get: RouteRegistrar; // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts // // @internal getRoutes: () => RouterRoute[]; - post:

    (route: RouteConfig, handler: RequestHandler) => void; - put:

    (route: RouteConfig, handler: RequestHandler) => void; + handleLegacyErrors:

    (handler: RequestHandler) => RequestHandler; + post: RouteRegistrar; + put: RouteRegistrar; routerPath: string; } @@ -1099,6 +1100,9 @@ export interface RouteConfigOptions { // @public export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +// @public +export type RouteRegistrar =

    (route: RouteConfig, handler: RequestHandler) => void; + // @public (undocumented) export interface SavedObject { attributes: T; From cfed9c6c485f4812f7a2ebccbce09ce60a9c2d93 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 26 Nov 2019 10:35:56 +0000 Subject: [PATCH 077/128] [Task Manager] Tests for the ability to run tasks of varying durations in parallel (#51572) This PR adds a test that ensures Task Manager is capable of picking up new tasks in parallel to a long running tasks that might otherwise hold up task execution. This doesn't add functionality - just a missing test case. --- .../plugins/task_manager/index.js | 17 ++++- .../plugins/task_manager/init_routes.js | 71 +++++++++++++++---- .../task_manager/task_manager_integration.js | 53 +++++++++++++- 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js index 938324c12a377..73253224bb45d 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js @@ -4,9 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +const { EventEmitter } = require('events'); + import { initRoutes } from './init_routes'; + +const once = function (emitter, event) { + return new Promise(resolve => { + emitter.once(event, resolve); + }); +}; + export default function TaskTestingAPI(kibana) { + const taskTestingEvents = new EventEmitter(); + return new kibana.Plugin({ name: 'sampleTask', require: ['elasticsearch', 'task_manager'], @@ -52,6 +63,10 @@ export default function TaskTestingAPI(kibana) { refresh: true, }); + if (params.waitForEvent) { + await once(taskTestingEvents, params.waitForEvent); + } + return { state: { count: (prevState.count || 0) + 1 }, runAt: millisecondsFromNow(params.nextRunMilliseconds), @@ -88,7 +103,7 @@ export default function TaskTestingAPI(kibana) { }, }); - initRoutes(server); + initRoutes(server, taskTestingEvents); }, }); } diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js index a9dfabae6d609..7b9e265a15d6f 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js @@ -23,11 +23,44 @@ const taskManagerQuery = { } }; -export function initRoutes(server) { +export function initRoutes(server, taskTestingEvents) { const taskManager = server.plugins.task_manager; server.route({ - path: '/api/sample_tasks', + path: '/api/sample_tasks/schedule', + method: 'POST', + config: { + validate: { + payload: Joi.object({ + task: Joi.object({ + taskType: Joi.string().required(), + interval: Joi.string().optional(), + params: Joi.object().required(), + state: Joi.object().optional(), + id: Joi.string().optional() + }) + }), + }, + }, + async handler(request) { + try { + const { task: taskFields } = request.payload; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskResult = await (taskManager.schedule(task, { request })); + + return taskResult; + } catch (err) { + return err; + } + }, + }); + + server.route({ + path: '/api/sample_tasks/ensure_scheduled', method: 'POST', config: { validate: { @@ -38,26 +71,19 @@ export function initRoutes(server) { params: Joi.object().required(), state: Joi.object().optional(), id: Joi.string().optional() - }), - ensureScheduled: Joi.boolean() - .default(false) - .optional(), + }) }), }, }, async handler(request) { try { - const { ensureScheduled = false, task: taskFields } = request.payload; + const { task: taskFields } = request.payload; const task = { ...taskFields, scope: [scope], }; - const taskResult = await ( - ensureScheduled - ? taskManager.ensureScheduled(task, { request }) - : taskManager.schedule(task, { request }) - ); + const taskResult = await (taskManager.ensureScheduled(task, { request })); return taskResult; } catch (err) { @@ -66,6 +92,27 @@ export function initRoutes(server) { }, }); + server.route({ + path: '/api/sample_tasks/event', + method: 'POST', + config: { + validate: { + payload: Joi.object({ + event: Joi.string().required() + }), + }, + }, + async handler(request) { + try { + const { event } = request.payload; + taskTestingEvents.emit(event); + return { event }; + } catch (err) { + return err; + } + }, + }); + server.route({ path: '/api/sample_tasks', method: 'GET', diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index 9b4297e995cbd..986648f795da6 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -58,7 +58,7 @@ export default function ({ getService }) { } function scheduleTask(task) { - return supertest.post('/api/sample_tasks') + return supertest.post('/api/sample_tasks/schedule') .set('kbn-xsrf', 'xxx') .send({ task }) .expect(200) @@ -66,13 +66,20 @@ export default function ({ getService }) { } function scheduleTaskIfNotExists(task) { - return supertest.post('/api/sample_tasks') + return supertest.post('/api/sample_tasks/ensure_scheduled') .set('kbn-xsrf', 'xxx') - .send({ task, ensureScheduled: true }) + .send({ task }) .expect(200) .then((response) => response.body); } + function releaseTasksWaitingForEventToComplete(event) { + return supertest.post('/api/sample_tasks/event') + .set('kbn-xsrf', 'xxx') + .send({ event }) + .expect(200); + } + it('should support middleware', async () => { const historyItem = _.random(1, 100); @@ -204,5 +211,45 @@ export default function ({ getService }) { expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.greaterThan(expectedDiff - buffer); expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.lessThan(expectedDiff + buffer); } + + it('should run tasks in parallel, allowing for long running tasks along side faster tasks', async () => { + /** + * It's worth noting this test relies on the /event endpoint that forces Task Manager to hold off + * on completing a task until a call is made by the test suite. + * If we begin testing with multiple Kibana instacnes in Parallel this will likely become flaky. + * If you end up here because the test is flaky, this might be why. + */ + const fastTask = await scheduleTask({ + taskType: 'sampleTask', + interval: `1s`, + params: { }, + }); + + const longRunningTask = await scheduleTask({ + taskType: 'sampleTask', + interval: `1s`, + params: { + waitForEvent: 'rescheduleHasHappened' + }, + }); + + function getTaskById(tasks, id) { + return tasks.filter(task => task.id === id)[0]; + } + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + expect(getTaskById(tasks, fastTask.id).state.count).to.eql(2); + }); + + await releaseTasksWaitingForEventToComplete('rescheduleHasHappened'); + + await retry.try(async () => { + const tasks = (await currentTasks()).docs; + + expect(getTaskById(tasks, fastTask.id).state.count).to.greaterThan(2); + expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); + }); + }); }); } From 9e168391affcf75c0762fccf1c90bbc009413cf8 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 26 Nov 2019 13:45:37 +0200 Subject: [PATCH 078/128] =?UTF-8?q?Move=20apply=20filters=20popover=20?= =?UTF-8?q?=E2=87=92=20NP=20(#51566)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move ApplyFiltersPopoverContent and applyFiltersPopover to NP * code review --- src/core/MIGRATION.md | 2 +- .../filter/action/apply_filter_action.ts | 2 +- .../apply_filters/apply_filters_popover.tsx | 74 ------------------- src/legacy/core_plugins/data/public/index.ts | 1 - .../apply_filter_popover_content.tsx | 11 +-- .../apply_filters/apply_filters_popover.tsx} | 23 +++++- .../data/public/ui}/apply_filters/index.ts | 2 +- src/plugins/data/public/ui/index.ts | 3 +- 8 files changed, 30 insertions(+), 88 deletions(-) delete mode 100644 src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/apply_filters/apply_filter_popover_content.tsx (94%) rename src/{legacy/core_plugins/data/public/filter/index.tsx => plugins/data/public/ui/apply_filters/apply_filters_popover.tsx} (58%) rename src/{legacy/core_plugins/data/public/filter => plugins/data/public/ui}/apply_filters/index.ts (92%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 22c96110742e0..c5e04c3cfb53a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1167,7 +1167,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | Directive is deprecated. | +| `import 'ui/apply_filters'` | `import { applyFiltersPopover } from '../data/public'` | Directive is deprecated. | | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | diff --git a/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts b/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts index 39ec1f78b65f0..946b3997a9712 100644 --- a/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts +++ b/src/legacy/core_plugins/data/public/filter/action/apply_filter_action.ts @@ -29,10 +29,10 @@ import { esFilters, FilterManager, TimefilterContract, + applyFiltersPopover, changeTimeFilter, extractTimeFilter, } from '../../../../../../plugins/data/public'; -import { applyFiltersPopover } from '../apply_filters/apply_filters_popover'; import { IndexPatternsStart } from '../../index_patterns'; export const GLOBAL_APPLY_FILTER_ACTION = 'GLOBAL_APPLY_FILTER_ACTION'; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx deleted file mode 100644 index 41f757e726c40..0000000000000 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; -import React, { Component } from 'react'; -import { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; -import { IndexPattern } from '../../index_patterns/index_patterns'; -import { esFilters } from '../../../../../../plugins/data/public'; - -interface Props { - filters: esFilters.Filter[]; - onCancel: () => void; - onSubmit: (filters: esFilters.Filter[]) => void; - indexPatterns: IndexPattern[]; -} - -interface State { - isFilterSelected: boolean[]; -} - -export class ApplyFiltersPopover extends Component { - public render() { - if (!this.props.filters || this.props.filters.length === 0) { - return ''; - } - - return ( - - - - - - ); - } -} - -type cancelFunction = () => void; -type submitFunction = (filters: esFilters.Filter[]) => void; -export const applyFiltersPopover = ( - filters: esFilters.Filter[], - indexPatterns: IndexPattern[], - onCancel: cancelFunction, - onSubmit: submitFunction -) => { - return ( - - ); -}; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index b33aef75e6756..c1b4226e6e49f 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -29,7 +29,6 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart }; -export { ApplyFiltersPopover } from './filter'; export { Field, FieldType, diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx similarity index 94% rename from src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx rename to src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx index 954cbca8f054b..affbb8acecb20 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx @@ -30,17 +30,12 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { IndexPattern } from '../../index_patterns'; -import { - mapAndFlattenFilters, - esFilters, - utils, - FilterLabel, -} from '../../../../../../plugins/data/public'; +import { mapAndFlattenFilters, esFilters, utils, IIndexPattern } from '../..'; +import { FilterLabel } from '../filter_bar'; interface Props { filters: esFilters.Filter[]; - indexPatterns: IndexPattern[]; + indexPatterns: IIndexPattern[]; onCancel: () => void; onSubmit: (filters: esFilters.Filter[]) => void; } diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx similarity index 58% rename from src/legacy/core_plugins/data/public/filter/index.tsx rename to src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx index e48a18fc53a76..71a042adffa39 100644 --- a/src/legacy/core_plugins/data/public/filter/index.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx @@ -17,4 +17,25 @@ * under the License. */ -export { ApplyFiltersPopover } from './apply_filters'; +import React from 'react'; +import { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; +import { IIndexPattern, esFilters } from '../..'; + +type CancelFnType = () => void; +type SubmitFnType = (filters: esFilters.Filter[]) => void; + +export const applyFiltersPopover = ( + filters: esFilters.Filter[], + indexPatterns: IIndexPattern[], + onCancel: CancelFnType, + onSubmit: SubmitFnType +) => { + return ( + + ); +}; diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts b/src/plugins/data/public/ui/apply_filters/index.ts similarity index 92% rename from src/legacy/core_plugins/data/public/filter/apply_filters/index.ts rename to src/plugins/data/public/ui/apply_filters/index.ts index 6b64230ed6a0c..93c1245e1ffb0 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/index.ts +++ b/src/plugins/data/public/ui/apply_filters/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { ApplyFiltersPopover } from './apply_filters_popover'; +export { applyFiltersPopover } from './apply_filters_popover'; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index d0aaf2f6aac1c..79107d1ede676 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export * from './filter_bar'; +export { FilterBar } from './filter_bar'; +export { applyFiltersPopover } from './apply_filters'; From 5dc24c9ee5d9ce68e492974d0ab9e0f6b92ec7e5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Nov 2019 12:49:30 +0100 Subject: [PATCH 079/128] Define spec provider for ActiveMQ meatricbeat module (#51698) --- .../tutorial_resources/logos/activemq.svg | 31 ++++++++++ .../tutorials/activemq_metrics/index.js | 61 +++++++++++++++++++ .../kibana/server/tutorials/register.js | 2 + 3 files changed, 94 insertions(+) create mode 100644 src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg create mode 100644 src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg new file mode 100644 index 0000000000000..20694ba6e62c7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js new file mode 100644 index 0000000000000..b76a9ee7c4dbe --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/activemq_metrics/index.js @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; + +export function activemqMetricsSpecProvider(context) { + const moduleName = 'activemq'; + return { + id: 'activemqMetrics', + name: i18n.translate('kbn.server.tutorials.activemqMetrics.nameTitle', { + defaultMessage: 'ActiveMQ metrics', + }), + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: i18n.translate('kbn.server.tutorials.activemqMetrics.shortDescription', { + defaultMessage: 'Fetch monitoring metrics from ActiveMQ instances.', + }), + longDescription: i18n.translate('kbn.server.tutorials.activemqMetrics.longDescription', { + defaultMessage: 'The `activemq` Metricbeat module fetches monitoring metrics from ActiveMQ instances \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-activemq.html', + }, + }), + euiIconType: '/plugins/kibana/home/tutorial_resources/logos/activemq.svg', + isBeta: true, + artifacts: { + application: { + label: i18n.translate('kbn.server.tutorials.corednsMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), + path: '/app/kibana#/discover' + }, + dashboards: [], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-activemq.html' + } + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, null, null, null, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index 2d1aaa92b1e26..f36909e59f39b 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -80,6 +80,7 @@ import { consulMetricsSpecProvider } from './consul_metrics'; import { cockroachdbMetricsSpecProvider } from './cockroachdb_metrics'; import { traefikMetricsSpecProvider } from './traefik_metrics'; import { awsLogsSpecProvider } from './aws_logs'; +import { activemqMetricsSpecProvider } from './activemq_metrics'; export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(systemLogsSpecProvider); @@ -146,4 +147,5 @@ export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(cockroachdbMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(traefikMetricsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(awsLogsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqMetricsSpecProvider); } From 0557a40a9db15def5ab1d55415e57114610ad111 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 26 Nov 2019 12:56:31 +0100 Subject: [PATCH 080/128] Document @kbn/config-schema. (#50307) --- .github/CODEOWNERS | 1 + packages/kbn-config-schema/README.md | 511 +++++++++++++++++++++++++++ 2 files changed, 512 insertions(+) create mode 100644 packages/kbn-config-schema/README.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e2abd5a3db1c..e208dc73c7b4b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -61,6 +61,7 @@ /config/kibana.yml @elastic/kibana-platform /x-pack/plugins/features/ @elastic/kibana-platform /x-pack/plugins/licensing/ @elastic/kibana-platform +/packages/kbn-config-schema/ @elastic/kibana-platform # Security /x-pack/legacy/plugins/security/ @elastic/kibana-security diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md new file mode 100644 index 0000000000000..8ba2c43b5e1fe --- /dev/null +++ b/packages/kbn-config-schema/README.md @@ -0,0 +1,511 @@ +# `@kbn/config-schema` — The Kibana config validation library + +`@kbn/config-schema` is a TypeScript library inspired by Joi and designed to allow run-time validation of the +Kibana configuration entries providing developers with a fully typed model of the validated data. + +## Table of Contents + +- [Why `@kbn/config-schema`?](#why-kbnconfig-schema) +- [Schema building blocks](#schema-building-blocks) + - [Basic types](#basic-types) + - [`schema.string()`](#schemastring) + - [`schema.number()`](#schemanumber) + - [`schema.boolean()`](#schemaboolean) + - [`schema.literal()`](#schemaliteral) + - [Composite types](#composite-types) + - [`schema.arrayOf()`](#schemaarrayof) + - [`schema.object()`](#schemaobject) + - [`schema.recordOf()`](#schemarecordof) + - [`schema.mapOf()`](#schemamapof) + - [Advanced types](#advanced-types) + - [`schema.oneOf()`](#schemaoneof) + - [`schema.any()`](#schemaany) + - [`schema.maybe()`](#schemamaybe) + - [`schema.nullable()`](#schemanullable) + - [`schema.never()`](#schemanever) + - [`schema.uri()`](#schemauri) + - [`schema.byteSize()`](#schemabytesize) + - [`schema.duration()`](#schemaduration) + - [`schema.conditional()`](#schemaconditional) + - [References](#references) + - [`schema.contextRef()`](#schemacontextref) + - [`schema.siblingRef()`](#schemasiblingref) +- [Custom validation](#custom-validation) +- [Default values](#default-values) + +## Why `@kbn/config-schema`? + +Validation of externally supplied data is very important for Kibana. Especially if this data is used to configure how it operates. + +There are a number of reasons why we decided to roll our own solution for the configuration validation: + +* **Limited API surface** - having a future rich library is awesome, but it's a really hard task to audit such library and make sure everything is sane and secure enough. As everyone knows complexity is the enemy of security and hence we'd like to have a full control over what exactly we expose and commit to maintain. +* **Custom error messages** - detailed validation error messages are a great help to developers, but at the same time they can contain information that's way too sensitive to expose to everyone. We'd like to control these messages and make them only as detailed as really needed. For example, we don't want validation error messages to contain the passwords for internal users to show-up in the logs. These logs are commonly ingested into Elasticsearch, and accessible to a large number of users which shouldn't have access to the internal user's password. +* **Type information** - having run-time guarantees is great, but additionally having compile-time guarantees is even better. We'd like to provide developers with a fully typed model of the validated data so that it's harder to misuse it _after_ validation. +* **Upgradability** - no matter how well a validation library is implemented, it will have bugs and may need to be improved at some point anyway. Some external libraries are very well supported, some aren't or won't be in the future. It's always a risk to depend on an external party with their own release cadence when you need to quickly fix a security vulnerability in a patch version. We'd like to have a better control over lifecycle of such an important piece of our codebase. + +## Schema building blocks + +The schema is composed of one or more primitives depending on the shape of the data you'd like to validate. + +```typescript +const simpleStringSchema = schema.string(); +const moreComplexObjectSchema = schema.object({ name: schema.string() }); +``` + +Every schema instance has a `validate` method that is used to perform a validation of the data according to the schema. This method accepts three arguments: + +* `data: any` - **required**, data to be validated with the schema +* `context: Record` - **optional**, object whose properties can be referenced by the [context references](#schemacontextref) +* `namespace: string` - **optional**, arbitrary string that is used to prefix every error message thrown during validation + +```typescript +const valueSchema = schema.object({ + isEnabled: schema.boolean(), + env: schema.string({ defaultValue: schema.contextRef('envName') }), +}); + +expect(valueSchema.validate({ isEnabled: true, env: 'prod' })).toEqual({ + isEnabled: true, + env: 'prod', +}); + +// Use default value for `env` from context via reference +expect(valueSchema.validate({ isEnabled: true }, { envName: 'staging' })).toEqual({ + isEnabled: true, + env: 'staging', +}); + +// Fail because of type mismatch +expect(() => + valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }) +).toThrowError( + '[isEnabled]: expected value of type [boolean] but got [string]' +); + +// Fail because of type mismatch and prefix error with a custom namespace +expect(() => + valueSchema.validate({ isEnabled: 'non-bool' }, { envName: 'staging' }, 'configuration') +).toThrowError( + '[configuration.isEnabled]: expected value of type [boolean] but got [string]' +); +``` + +__Notes:__ +* `validate` method throws as soon as the first schema violation is encountered, no further validation is performed. +* when you retrieve configuration within a Kibana plugin `validate` function is called by the Core automatically providing appropriate namespace and context variables (environment name, package info etc.). + +### Basic types + +#### `schema.string()` + +Validates input data as a string. + +__Output type:__ `string` + +__Options:__ + * `defaultValue: string | Reference | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `minLength: number` - defines a minimum length the string should have. + * `maxLength: number` - defines a maximum length the string should have. + * `hostname: boolean` - indicates whether the string should be validated as a valid hostname (per [RFC 1123](https://tools.ietf.org/html/rfc1123)). + +__Usage:__ +```typescript +const valueSchema = schema.string({ maxLength: 10 }); +``` + +__Notes:__ +* By default `schema.string()` allows empty strings, to prevent that use non-zero value for `minLength` option. +* To validate a string using a regular expression use a custom validator function, see [Custom validation](#custom-validation) section for more details. + +#### `schema.number()` + +Validates input data as a number. + +__Output type:__ `number` + +__Options:__ + * `defaultValue: number | Reference | (() => number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `min: number` - defines a minimum value the number should have. + * `max: number` - defines a maximum value the number should have. + +__Usage:__ +```typescript +const valueSchema = schema.number({ max: 10 }); +``` + +__Notes:__ +* The `schema.number()` also supports a string as input if it can be safely coerced into number. + +#### `schema.boolean()` + +Validates input data as a boolean. + +__Output type:__ `boolean` + +__Options:__ + * `defaultValue: boolean | Reference | (() => boolean)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: boolean) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.boolean({ defaultValue: false }); +``` + +#### `schema.literal()` + +Validates input data as a [string](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types), [numeric](https://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types) or boolean literal. + +__Output type:__ `string`, `number` or `boolean` literals + +__Options:__ + * `defaultValue: TLiteral | Reference | (() => TLiteral)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TLiteral) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = [ + schema.literal('stringLiteral'), + schema.literal(100500), + schema.literal(false), +]; +``` + +### Composite types + +#### `schema.arrayOf()` + +Validates input data as a homogeneous array with the values being validated against predefined schema. + +__Output type:__ `TValue[]` + +__Options:__ + * `defaultValue: TValue[] | Reference | (() => TValue[])` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TValue[]) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `minSize: number` - defines a minimum size the array should have. + * `maxSize: number` - defines a maximum size the array should have. + +__Usage:__ +```typescript +const valueSchema = schema.arrayOf(schema.number()); +``` + +#### `schema.object()` + +Validates input data as an object with a predefined set of properties. + +__Output type:__ `{ [K in keyof TProps]: TypeOf } as TObject` + +__Options:__ + * `defaultValue: TObject | Reference | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `allowUnknowns: boolean` - indicates whether unknown object properties should be allowed. It's `false` by default. + +__Usage:__ +```typescript +const valueSchema = schema.object({ + isEnabled: schema.boolean({ defaultValue: false }), + name: schema.string({ minLength: 10 }), +}); +``` + +__Notes:__ +* Using `allowUnknowns` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. +* Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. + +#### `schema.recordOf()` + +Validates input data as an object with the keys and values being validated against predefined schema. + +__Output type:__ `Record` + +__Options:__ + * `defaultValue: Record | Reference> | (() => Record)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: Record) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.recordOf(schema.string(), schema.number()); +``` + +__Notes:__ +* You can use a union of literal types as a record's key schema to restrict record to a specific set of keys, e.g. `schema.oneOf([schema.literal('isEnabled'), schema.literal('name')])`. + +#### `schema.mapOf()` + +Validates input data as a map with the keys and values being validated against the predefined schema. + +__Output type:__ `Map` + +__Options:__ + * `defaultValue: Map | Reference> | (() => Map)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: Map) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.mapOf(schema.string(), schema.number()); +``` + +### Advanced types + +#### `schema.oneOf()` + +Allows a list of alternative schemas to validate input data against. + +__Output type:__ `TValue1 | TValue2 | TValue3 | ..... as TUnion` + +__Options:__ + * `defaultValue: TUnion | Reference | (() => TUnion)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TUnion) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.oneOf([schema.literal('∞'), schema.number()]); +``` + +__Notes:__ +* Since the result data type is a type union you should use various TypeScript type guards to get the exact type. + +#### `schema.any()` + +Indicates that input data shouldn't be validated and returned as is. + +__Output type:__ `any` + +__Options:__ + * `defaultValue: any | Reference | (() => any)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: any) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.any(); +``` + +__Notes:__ +* `schema.any()` is essentially an escape hatch for the case when your data can __really__ have any type and should be avoided at all costs. + +#### `schema.maybe()` + +Indicates that input data is optional and may not be present. + +__Output type:__ `T | undefined` + +__Usage:__ +```typescript +const valueSchema = schema.maybe(schema.string()); +``` + +__Notes:__ +* Don't use `schema.maybe()` if a nested type defines a default value. + +#### `schema.nullable()` + +Indicates that input data is optional and defaults to `null` if it's not present. + +__Output type:__ `T | null` + +__Usage:__ +```typescript +const valueSchema = schema.nullable(schema.string()); +``` + +__Notes:__ +* `schema.nullable()` also treats explicitly specified `null` as a valid input. + +#### `schema.never()` + +Indicates that input data is forbidden. + +__Output type:__ `never` + +__Usage:__ +```typescript +const valueSchema = schema.never(); +``` + +__Notes:__ +* `schema.never()` has a very limited application and usually used within [conditional schemas](#schemaconditional) to fully or partially forbid input data. + +#### `schema.uri()` + +Validates input data as a proper URI string (per [RFC 3986](https://tools.ietf.org/html/rfc3986)). + +__Output type:__ `string` + +__Options:__ + * `defaultValue: string | Reference | (() => string)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: string) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `scheme: string | string[]` - limits allowed URI schemes to the one(s) defined here. + +__Usage:__ +```typescript +const valueSchema = schema.uri({ scheme: 'https' }); +``` + +__Notes:__ +* Prefer using `schema.uri()` for all URI validations even though it may be possible to replicate it with a custom validator for `schema.string()`. + +#### `schema.byteSize()` + +Validates input data as a proper digital data size. + +__Output type:__ `ByteSizeValue` + +__Options:__ + * `defaultValue: ByteSizeValue | string | number | Reference | (() => ByteSizeValue | string | number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: ByteSizeValue | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + * `min: ByteSizeValue | string | number` - defines a minimum value the size should have. + * `max: ByteSizeValue | string | number` - defines a maximum value the size should have. + +__Usage:__ +```typescript +const valueSchema = schema.byteSize({ min: '3kb' }); +``` + +__Notes:__ +* The string value for `schema.byteSize()` and its options supports the following prefixes: `b`, `kb`, `mb`, `gb` and `tb`. +* The number value is treated as a number of bytes and hence should be a positive integer, e.g. `100` is equal to `'100b'`. +* Currently you cannot specify zero bytes with a string format and should use number `0` instead. + +#### `schema.duration()` + +Validates input data as a proper [duration](https://momentjs.com/docs/#/durations/). + +__Output type:__ `moment.Duration` + +__Options:__ + * `defaultValue: moment.Duration | string | number | Reference | (() => moment.Duration | string | number)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: moment.Duration | string | number) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.duration({ defaultValue: '70ms' }); +``` + +__Notes:__ +* The string value for `schema.duration()` supports the following prefixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. +* The number value is treated as a number of milliseconds and hence should be a positive integer, e.g. `100` is equal to `'100ms'`. + +#### `schema.conditional()` + +Allows a specified condition that is evaluated _at the validation time_ and results in either one or another input validation schema. + +The first argument is always a [reference](#references) while the second one can be: +* another reference, in this cases both references are "dereferenced" and compared +* schema, in this case the schema is used to validate "dereferenced" value of the first reference +* value, in this case "dereferenced" value of the first reference is compared to that value + +The third argument is a schema that should be used if the result of the aforementioned comparison evaluates to `true`, otherwise `schema.conditional()` should fallback +to the schema provided as the fourth argument. + +__Output type:__ `TTrueResult | TFalseResult` + +__Options:__ + * `defaultValue: TTrueResult | TFalseResult | Reference | (() => TTrueResult | TFalseResult` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TTrueResult | TFalseResult) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.object({ + key: schema.oneOf([schema.literal('number'), schema.literal('string')]), + value: schema.conditional(schema.siblingRef('key'), 'number', schema.number(), schema.string()), +}); +``` + +__Notes:__ +* Conditional schemas may be hard to read and understand and hence should be used only sparingly. + +### References + +#### `schema.contextRef()` + +Defines a reference to the value specified through the validation context. Context reference is only used as part of a [conditional schema](#schemaconditional) or as a default value for any other schema. + +__Output type:__ `TReferenceValue` + +__Usage:__ +```typescript +const valueSchema = schema.object({ + env: schema.string({ defaultValue: schema.contextRef('envName') }), +}); +valueSchema.validate({}, { envName: 'dev' }); +``` + +__Notes:__ +* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. +* The root context that Kibana provides during config validation includes lots of useful properties like `environment name` that can be used to provide a strict schema for production and more relaxed one for development. + +#### `schema.siblingRef()` + +Defines a reference to the value of the sibling key. Sibling references are only used a part of [conditional schema](#schemaconditional) or as a default value for any other schema. + +__Output type:__ `TReferenceValue` + +__Usage:__ +```typescript +const valueSchema = schema.object({ + node: schema.object({ tag: schema.string() }), + env: schema.string({ defaultValue: schema.siblingRef('node.tag') }), +}); +``` + +__Notes:__ +* The `@kbn/config-schema` neither validates nor coerces the "dereferenced" value and the developer is responsible for making sure that it has the appropriate type. + +## Custom validation + +Using built-in schema primitives may not be enough in some scenarios or sometimes the attempt to model complex schemas with built-in primitives only may result in unreadable code. +For these cases `@kbn/config-schema` provides a way to specify a custom validation function for almost any schema building block through the `validate` option. + +For example `@kbn/config-schema` doesn't have a dedicated primitive for the `RegExp` based validation currently, but you can easily do that with a custom `validate` function: + +```typescript +const valueSchema = schema.string({ + minLength: 3, + validate(value) { + if (!/^[a-z0-9_-]+$/.test(value)) { + return `must be lower case, a-z, 0-9, '_', and '-' are allowed`; + } + }, +}); + +// ...or if you use that construct a lot... + +const regexSchema = (regex: RegExp) => schema.string({ + validate: value => regex.test(value) ? undefined : `must match "${regex.toString()}"`, +}); +const valueSchema = regexSchema(/^[a-z0-9_-]+$/); +``` + +Custom validation function is run _only after_ all built-in validations passed. It should either return a `string` as an error message +to denote the failed validation or not return anything at all (`void`) otherwise. Please also note that `validate` function is synchronous. + +Another use case for custom validation functions is when the schema depends on some run-time data: + +```typescript +const gesSchema = randomRunTimeSeed => schema.string({ + validate: value => value !== randomRunTimeSeed ? 'value is not allowed' : undefined +}); + +const schema = gesSchema('some-random-run-time-data'); +``` + +## Default values + +If you have an optional config field that you can have a default value for you may want to consider using dedicated `defaultValue` option to not +deal with "defined or undefined"-like checks all over the place in your code. You have three options to provide a default value for almost any schema primitive: + +* plain value that's known at the compile time +* [reference](#references) to a value that will be "dereferenced" at the validation time +* function that is invoked at the validation time and returns a plain value + +```typescript +const valueSchemaWithPlainValueDefault = schema.string({ defaultValue: 'n/a' }); +const valueSchemaWithReferencedValueDefault = schema.string({ defaultValue: schema.contextRef('env') }); +const valueSchemaWithFunctionEvaluatedDefault = schema.string({ defaultValue: () => Math.random().toString() }); +``` + +__Notes:__ +* `@kbn/config-schema` neither validates nor coerces default value and developer is responsible for making sure that it has the appropriate type. From e8e517475ae54b0276070c5dc1b7c4b0ac439fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 26 Nov 2019 13:19:11 +0100 Subject: [PATCH 081/128] [Security] Add message to login page (#51557) * [Security] Add loginAssistanceMessage to login page * Fix tests * Fix login_page.test.tsx * Fix defaultValue * Render login assistance message independently of other messages and use EuiText instead of EuiCallOut * Use small text Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com> * Flip order of message around --- docs/settings/security-settings.asciidoc | 7 +- .../resources/bin/kibana-docker | 1 + x-pack/legacy/plugins/security/index.js | 3 + .../basic_login_form.test.tsx.snap | 14 ++++ .../basic_login_form.test.tsx | 3 + .../basic_login_form/basic_login_form.tsx | 15 ++++ .../__snapshots__/login_page.test.tsx.snap | 83 +++++++++++++++++++ .../components/login_page/login_page.test.tsx | 19 +++++ .../components/login_page/login_page.tsx | 1 + .../security/public/views/login/login.tsx | 4 +- x-pack/plugins/security/server/config.test.ts | 73 ++++++++-------- x-pack/plugins/security/server/config.ts | 1 + x-pack/plugins/security/server/plugin.test.ts | 1 + x-pack/plugins/security/server/plugin.ts | 1 + 14 files changed, 188 insertions(+), 38 deletions(-) diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index b852d38c05dc9..805d991a9a0f3 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -20,7 +20,7 @@ are enabled. Do not set this to `false`; it disables the login form, user and role management screens, and authorization using <>. To disable {security-features} entirely, see -{ref}/security-settings.html[{es} security settings]. +{ref}/security-settings.html[{es} security settings]. `xpack.security.audit.enabled`:: Set to `true` to enable audit logging for security events. By default, it is set @@ -40,7 +40,7 @@ An arbitrary string of 32 characters or more that is used to encrypt credentials in a cookie. It is crucial that this key is not exposed to users of {kib}. By default, a value is automatically generated in memory. If you use that default behavior, all sessions are invalidated when {kib} restarts. -In addition, high-availability deployments of {kib} will behave unexpectedly +In addition, high-availability deployments of {kib} will behave unexpectedly if this setting isn't the same for all instances of {kib}. `xpack.security.secureCookies`:: @@ -53,3 +53,6 @@ routing requests through a load balancer or proxy). Sets the session duration (in milliseconds). By default, sessions stay active until the browser is closed. When this is set to an explicit timeout, closing the browser still requires the user to log back in to {kib}. + +`xpack.security.loginAssistanceMessage`:: +Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 6609b905b81ec..497307fa4124b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -180,6 +180,7 @@ kibana_vars=( xpack.security.encryptionKey xpack.security.secureCookies xpack.security.sessionTimeout + xpack.security.loginAssistanceMessage telemetry.enabled telemetry.sendUsageFrom ) diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index c098e3e67a6d9..d147c2572ceeb 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -31,6 +31,7 @@ export const security = (kibana) => new kibana.Plugin({ encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'), sessionTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + loginAssistanceMessage: Joi.string().default(), authorization: Joi.object({ legacyFallback: Joi.object({ enabled: Joi.boolean().default(true) // deprecated @@ -147,7 +148,9 @@ export const security = (kibana) => new kibana.Plugin({ server.injectUiAppVars('login', () => { const { showLogin, allowLogin, layout = 'form' } = securityPlugin.__legacyCompat.license.getFeatures(); + const { loginAssistanceMessage } = securityPlugin.__legacyCompat.config; return { + loginAssistanceMessage, loginState: { showLogin, allowLogin, diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap index 3b3024024a9cf..a08c454e569e6 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap @@ -2,6 +2,20 @@ exports[`BasicLoginForm renders as expected 1`] = ` + + +

    { loginState={loginState} next={''} intl={null as any} + loginAssistanceMessage="" /> ) ).toMatchSnapshot(); @@ -68,6 +69,7 @@ describe('BasicLoginForm', () => { next={''} infoMessage={'Hey this is an info message'} intl={null as any} + loginAssistanceMessage="" /> ); @@ -86,6 +88,7 @@ describe('BasicLoginForm', () => { loginState={loginState} next={''} intl={null as any} + loginAssistanceMessage="" /> ); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index 9dbb556f5f5f4..acdc29842d4c6 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -7,6 +7,8 @@ import { EuiButton, EuiCallOut, EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { EuiText } from '@elastic/eui'; import { LoginState } from '../../../../../common/login_state'; interface Props { @@ -16,6 +18,7 @@ interface Props { loginState: LoginState; next: string; intl: InjectedIntl; + loginAssistanceMessage: string; } interface State { @@ -38,6 +41,7 @@ class BasicLoginFormUI extends Component { public render() { return ( + {this.renderLoginAssistanceMessage()} {this.renderMessage()} @@ -102,6 +106,16 @@ class BasicLoginFormUI extends Component { ); } + private renderLoginAssistanceMessage = () => { + return ( + + + {this.props.loginAssistanceMessage} + + + ); + }; + private renderMessage = () => { if (this.state.message) { return ( @@ -132,6 +146,7 @@ class BasicLoginFormUI extends Component { ); } + return null; }; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap index fc33c6e0a82cc..17ba81988414a 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/__snapshots__/login_page.test.tsx.snap @@ -160,6 +160,88 @@ exports[`LoginPage disabled form states renders as expected when an unknown logi
    `; +exports[`LoginPage disabled form states renders as expected when loginAssistanceMessage is set 1`] = ` +
    +
    +
    + + + + + +

    + +

    +
    + +

    + +

    +
    + +
    +
    +
    + + + + + +
    +
    +`; + exports[`LoginPage disabled form states renders as expected when secure cookies are required but not present 1`] = `
    { loginState: createLoginState(), isSecureConnection: false, requiresSecureConnection: true, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -61,6 +62,7 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -76,6 +78,7 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); @@ -91,6 +94,21 @@ describe('LoginPage', () => { }), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', + }; + + expect(shallow()).toMatchSnapshot(); + }); + + it('renders as expected when loginAssistanceMessage is set', () => { + const props = { + http: createMockHttp(), + window: {}, + next: '', + loginState: createLoginState(), + isSecureConnection: false, + requiresSecureConnection: false, + loginAssistanceMessage: 'This is an *important* message', }; expect(shallow()).toMatchSnapshot(); @@ -106,6 +124,7 @@ describe('LoginPage', () => { loginState: createLoginState(), isSecureConnection: false, requiresSecureConnection: false, + loginAssistanceMessage: '', }; expect(shallow()).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx index 82dd0e679a5ee..e7e56947ca58f 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx @@ -31,6 +31,7 @@ interface Props { loginState: LoginState; isSecureConnection: boolean; requiresSecureConnection: boolean; + loginAssistanceMessage: string; } export class LoginPage extends Component { diff --git a/x-pack/legacy/plugins/security/public/views/login/login.tsx b/x-pack/legacy/plugins/security/public/views/login/login.tsx index 8b452e4c4fdf5..d9daf2d1f4d0d 100644 --- a/x-pack/legacy/plugins/security/public/views/login/login.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/login.tsx @@ -39,7 +39,8 @@ interface AnyObject { $http: AnyObject, $window: AnyObject, secureCookies: boolean, - loginState: LoginState + loginState: LoginState, + loginAssistanceMessage: string ) => { const basePath = chrome.getBasePath(); const next = parseNext($window.location.href, basePath); @@ -59,6 +60,7 @@ interface AnyObject { loginState={loginState} isSecureConnection={isSecure} requiresSecureConnection={secureCookies} + loginAssistanceMessage={loginAssistanceMessage} next={next} /> , diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 943d582bf484a..569611516c880 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -13,45 +13,48 @@ import { createConfig$, ConfigSchema } from './config'; describe('config schema', () => { it('generates proper defaults', () => { expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "sessionTimeout": null, + } + `); expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "sessionTimeout": null, + } + `); expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "loginAssistanceMessage": "", + "secureCookies": false, + "sessionTimeout": null, + } + `); }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 6fe3fc73e458c..a257a25344393 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -26,6 +26,7 @@ const providerOptionsSchema = (providerType: string, optionsSchema: Type) = export const ConfigSchema = schema.object( { + loginAssistanceMessage: schema.string({ defaultValue: '' }), cookieName: schema.string({ defaultValue: 'sid' }), encryptionKey: schema.conditional( schema.contextRef('dist'), diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index b0e2ae7176834..2ff0e915fc1b0 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -52,6 +52,7 @@ describe('Security Plugin', () => { ], }, "cookieName": "sid", + "loginAssistanceMessage": undefined, "secureCookies": true, "sessionTimeout": 1500, }, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 4b3997fe74f1b..c8761050524a5 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -205,6 +205,7 @@ export class Plugin { // We should stop exposing this config as soon as only new platform plugin consumes it. The only // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { + loginAssistanceMessage: config.loginAssistanceMessage, sessionTimeout: config.sessionTimeout, secureCookies: config.secureCookies, cookieName: config.cookieName, From 80879368a1f4dcc253b44b9406785ee668ba4778 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 26 Nov 2019 14:22:34 +0100 Subject: [PATCH 082/128] Typescriptify and shim kbn_tp_run_pipeline test plugin (#50645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Typscriptify and shim kbn_tp_run_pipeline test plugin * fix imports to not re-export ‘legacy’ from root of plugin --- scripts/functional_tests.js | 2 +- src/plugins/expressions/public/index.ts | 11 +- src/plugins/expressions/public/render.ts | 10 +- tasks/config/run.js | 2 +- test/functional/services/browser.ts | 5 +- test/interpreter_functional/README.md | 10 +- .../{config.js => config.ts} | 17 ++- .../{index.js => index.ts} | 26 ++-- .../plugins/kbn_tp_run_pipeline/public/app.js | 76 ----------- .../public/components/main.js | 91 ------------- .../kbn_tp_run_pipeline/public/index.ts | 20 +++ .../kbn_tp_run_pipeline/public/legacy.ts | 41 ++++++ .../public/np_ready/app/app.tsx | 28 ++++ .../public/np_ready/app/components/main.tsx | 122 +++++++++++++++++ .../public/np_ready/index.ts | 28 ++++ .../public/np_ready/plugin.ts | 45 +++++++ .../public/np_ready/services.ts | 23 ++++ .../public/np_ready/types.ts | 37 +++++ .../run_pipeline/{basic.js => basic.ts} | 55 +++++--- .../run_pipeline/{helpers.js => helpers.ts} | 126 +++++++++++++----- .../run_pipeline/{index.js => index.ts} | 11 +- .../run_pipeline/{metric.js => metric.ts} | 44 ++++-- .../{tag_cloud.js => tag_cloud.ts} | 39 ++++-- 23 files changed, 593 insertions(+), 276 deletions(-) rename test/interpreter_functional/{config.js => config.ts} (76%) rename test/interpreter_functional/plugins/kbn_tp_run_pipeline/{index.js => index.ts} (68%) delete mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js delete mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts create mode 100644 test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts rename test/interpreter_functional/test_suites/run_pipeline/{basic.js => basic.ts} (69%) rename test/interpreter_functional/test_suites/run_pipeline/{helpers.js => helpers.ts} (55%) rename test/interpreter_functional/test_suites/run_pipeline/{index.js => index.ts} (82%) rename test/interpreter_functional/test_suites/run_pipeline/{metric.js => metric.ts} (64%) rename test/interpreter_functional/test_suites/run_pipeline/{tag_cloud.js => tag_cloud.ts} (61%) diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 9f4e678c6adf5..b65cd3835cc0a 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -22,6 +22,6 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config.js'), require.resolve('../test/api_integration/config.js'), require.resolve('../test/plugin_functional/config.js'), - require.resolve('../test/interpreter_functional/config.js'), + require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), ]); diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index a14aaae98fc34..6dc88fd23f29a 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -20,10 +20,6 @@ import { PluginInitializerContext } from '../../../core/public'; import { ExpressionsPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new ExpressionsPublicPlugin(initializerContext); -} - export { ExpressionsPublicPlugin as Plugin }; export * from './plugin'; @@ -31,3 +27,10 @@ export * from './types'; export * from '../common'; export { interpreterProvider, ExpressionInterpret } from './interpreter_provider'; export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; +export { ExpressionDataHandler } from './execute'; + +export { RenderResult, ExpressionRenderHandler } from './render'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ExpressionsPublicPlugin(initializerContext); +} diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 364d5f587bb6f..3c7008806e779 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -30,15 +30,17 @@ interface RenderError { export type IExpressionRendererExtraHandlers = Record; +export type RenderResult = RenderId | RenderError; + export class ExpressionRenderHandler { - render$: Observable; + render$: Observable; update$: Observable; events$: Observable; private element: HTMLElement; private destroyFn?: any; private renderCount: number = 0; - private renderSubject: Rx.BehaviorSubject; + private renderSubject: Rx.BehaviorSubject; private eventsSubject: Rx.Subject; private updateSubject: Rx.Subject; private handlers: IInterpreterRenderHandlers; @@ -49,11 +51,11 @@ export class ExpressionRenderHandler { this.eventsSubject = new Rx.Subject(); this.events$ = this.eventsSubject.asObservable().pipe(share()); - this.renderSubject = new Rx.BehaviorSubject(null as RenderId | RenderError | null); + this.renderSubject = new Rx.BehaviorSubject(null as RenderResult | null); this.render$ = this.renderSubject.asObservable().pipe( share(), filter(_ => _ !== null) - ) as Observable; + ) as Observable; this.updateSubject = new Rx.Subject(); this.update$ = this.updateSubject.asObservable().pipe(share()); diff --git a/tasks/config/run.js b/tasks/config/run.js index ea5a4b01dc8a5..e4071c8b7d0ab 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -254,7 +254,7 @@ module.exports = function (grunt) { cmd: NODE, args: [ 'scripts/functional_tests', - '--config', 'test/interpreter_functional/config.js', + '--config', 'test/interpreter_functional/config.ts', '--bail', '--debug', '--kibana-install-dir', KIBANA_INSTALL_DIR, diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index a8ce4270d4205..ab686f4d5ffec 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -470,7 +470,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { ); } - public async executeAsync(fn: string | ((...args: any[]) => R), ...args: any[]): Promise { + public async executeAsync( + fn: string | ((...args: any[]) => Promise), + ...args: any[] + ): Promise { return await driver.executeAsyncScript( fn, ...cloneDeep(args, arg => { diff --git a/test/interpreter_functional/README.md b/test/interpreter_functional/README.md index 336bfe3405a01..73df0ce4c9f04 100644 --- a/test/interpreter_functional/README.md +++ b/test/interpreter_functional/README.md @@ -3,7 +3,7 @@ This folder contains interpreter functional tests. Add new test suites into the `test_suites` folder and reference them from the -`config.js` file. These test suites work the same as regular functional test. +`config.ts` file. These test suites work the same as regular functional test. ## Run the test @@ -11,17 +11,17 @@ To run these tests during development you can use the following commands: ``` # Start the test server (can continue running) -node scripts/functional_tests_server.js --config test/interpreter_functional/config.js +node scripts/functional_tests_server.js --config test/interpreter_functional/config.ts # Start a test run -node scripts/functional_test_runner.js --config test/interpreter_functional/config.js +node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts ``` # Writing tests -Look into test_suites/run_pipeline/basic.js for examples +Look into test_suites/run_pipeline/basic.ts for examples to update baseline screenshots and snapshots run with: ``` -node scripts/functional_test_runner.js --config test/interpreter_functional/config.js --updateBaselines +node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts --updateBaselines ``` \ No newline at end of file diff --git a/test/interpreter_functional/config.js b/test/interpreter_functional/config.ts similarity index 76% rename from test/interpreter_functional/config.js rename to test/interpreter_functional/config.ts index e8700262e273a..0fe7df4d50715 100644 --- a/test/interpreter_functional/config.js +++ b/test/interpreter_functional/config.ts @@ -19,25 +19,26 @@ import path from 'path'; import fs from 'fs'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; -export default async function ({ readConfigFile }) { +export default async function({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); - const plugins = allFiles.filter(file => fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory()); + const plugins = allFiles.filter(file => + fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory() + ); return { - testFiles: [ - require.resolve('./test_suites/run_pipeline'), - ], + testFiles: [require.resolve('./test_suites/run_pipeline')], services: functionalConfig.get('services'), pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), esTestCluster: functionalConfig.get('esTestCluster'), apps: functionalConfig.get('apps'), esArchiver: { - directory: path.resolve(__dirname, '../es_archives') + directory: path.resolve(__dirname, '../es_archives'), }, snapshots: { directory: path.resolve(__dirname, 'snapshots'), @@ -49,7 +50,9 @@ export default async function ({ readConfigFile }) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), - ...plugins.map(pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`), + ...plugins.map( + pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` + ), ], }, }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts similarity index 68% rename from test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js rename to test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts index 95d6a555ebcf0..1d5564ec06e4e 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.js +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/index.ts @@ -16,24 +16,34 @@ * specific language governing permissions and limitations * under the License. */ +import { Legacy } from 'kibana'; +import { + ArrayOrItem, + LegacyPluginApi, + LegacyPluginSpec, + LegacyPluginOptions, +} from 'src/legacy/plugin_discovery/types'; -export default function (kibana) { - return new kibana.Plugin({ +// eslint-disable-next-line import/no-default-export +export default function(kibana: LegacyPluginApi): ArrayOrItem { + const pluginSpec: Partial = { + id: 'kbn_tp_run_pipeline', uiExports: { app: { title: 'Run Pipeline', description: 'This is a sample plugin to test running pipeline expressions', - main: 'plugins/kbn_tp_run_pipeline/app', - } + main: 'plugins/kbn_tp_run_pipeline/legacy', + }, }, - init(server) { + init(server: Legacy.Server) { // The following lines copy over some configuration variables from Kibana // to this plugin. This will be needed when embedding visualizations, so that e.g. // region map is able to get its configuration. server.injectUiAppVars('kbn_tp_run_pipeline', async () => { - return await server.getInjectedUiAppVars('kibana'); + return server.getInjectedUiAppVars('kibana'); }); - } - }); + }, + }; + return new kibana.Plugin(pluginSpec); } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js deleted file mode 100644 index e9ab2a4169915..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; - -import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; -import { registries } from 'plugins/interpreter/registries'; -import { npStart } from 'ui/new_platform'; - -// This is required so some default styles and required scripts/Angular modules are loaded, -// or the timezone setting is correctly applied. -import 'ui/autoload/all'; - -// These are all the required uiExports you need to import in case you want to embed visualizations. -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visEditorTypes'; -import 'uiExports/visualize'; -import 'uiExports/savedObjectTypes'; -import 'uiExports/search'; -import 'uiExports/interpreter'; - -import { Main } from './components/main'; - -const app = uiModules.get('apps/kbnRunPipelinePlugin', ['kibana']); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -function RootController($scope, $element) { - const domNode = $element[0]; - - // render react to DOM - render(
    , domNode); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('kbnRunPipelinePlugin', RootController); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js deleted file mode 100644 index 3e19d3a4d78ec..0000000000000 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, -} from '@elastic/eui'; -import { first } from 'rxjs/operators'; - -class Main extends React.Component { - chartDiv = React.createRef(); - - constructor(props) { - super(props); - - this.state = { - expression: '', - }; - - window.runPipeline = async (expression, context = {}, initialContext = {}) => { - this.setState({ expression }); - const adapters = { - requests: new props.RequestAdapter(), - data: new props.DataAdapter(), - }; - return await props.expressions.execute(expression, { - inspectorAdapters: adapters, - context, - searchContext: initialContext, - }).getData(); - }; - - let lastRenderHandler; - window.renderPipelineResponse = async (context = {}) => { - if (lastRenderHandler) { - lastRenderHandler.destroy(); - } - - lastRenderHandler = props.expressions.render(this.chartDiv, context); - const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise(); - - if (typeof renderResult === 'object' && renderResult.type === 'error') { - return this.setState({ expression: 'Render error!\n\n' + JSON.stringify(renderResult.error) }); - } - }; - } - - - render() { - const pStyle = { - display: 'flex', - width: '100%', - height: '300px' - }; - - return ( - - - - - runPipeline tests are running ... - -
    this.chartDiv = ref} style={pStyle}/> -
    {this.state.expression}
    - - - - ); - } -} - -export { Main }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts new file mode 100644 index 0000000000000..c4cc7175d6157 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './np_ready'; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts new file mode 100644 index 0000000000000..39ce2b3077c96 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/legacy.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'src/core/public'; +import { npSetup, npStart } from 'ui/new_platform'; + +import { plugin } from './np_ready'; + +// This is required so some default styles and required scripts/Angular modules are loaded, +// or the timezone setting is correctly applied. +import 'ui/autoload/all'; +// Used to run esaggs queries +import 'uiExports/fieldFormats'; +import 'uiExports/search'; +import 'uiExports/visRequestHandlers'; +import 'uiExports/visResponseHandlers'; +// Used for kibana_context function + +import 'uiExports/savedObjectTypes'; +import 'uiExports/interpreter'; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx new file mode 100644 index 0000000000000..f47a7c3a256f0 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/app.tsx @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { Main } from './components/main'; + +export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => { + render(
    , element); + return () => unmountComponentAtNode(element); +}; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx new file mode 100644 index 0000000000000..c091765619a19 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui'; +import { first } from 'rxjs/operators'; +import { + RequestAdapter, + DataAdapter, +} from '../../../../../../../../src/plugins/inspector/public/adapters'; +import { + Adapters, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, +} from '../../types'; +import { getExpressions } from '../../services'; + +declare global { + interface Window { + runPipeline: ( + expressions: string, + context?: Context, + initialContext?: Context + ) => ReturnType; + renderPipelineResponse: (context?: Context) => Promise; + } +} + +interface State { + expression: string; +} + +class Main extends React.Component<{}, State> { + chartRef = React.createRef(); + + constructor(props: {}) { + super(props); + + this.state = { + expression: '', + }; + + window.runPipeline = async ( + expression: string, + context: Context = {}, + initialContext: Context = {} + ) => { + this.setState({ expression }); + const adapters: Adapters = { + requests: new RequestAdapter(), + data: new DataAdapter(), + }; + return getExpressions() + .execute(expression, { + inspectorAdapters: adapters, + context, + // TODO: naming / typing is confusing and doesn't match here + // searchContext is also a way to set initialContext and Context can't be set to SearchContext + searchContext: initialContext as any, + }) + .getData(); + }; + + let lastRenderHandler: ExpressionRenderHandler; + window.renderPipelineResponse = async (context = {}) => { + if (lastRenderHandler) { + lastRenderHandler.destroy(); + } + + lastRenderHandler = getExpressions().render(this.chartRef.current!, context); + const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise(); + + if (typeof renderResult === 'object' && renderResult.type === 'error') { + this.setState({ + expression: 'Render error!\n\n' + JSON.stringify(renderResult.error), + }); + } + + return renderResult; + }; + } + + render() { + const pStyle = { + display: 'flex', + width: '100%', + height: '300px', + }; + + return ( + + + + runPipeline tests are running ... +
    +
    {this.state.expression}
    + + + + ); + } +} + +export { Main }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts new file mode 100644 index 0000000000000..d7a764b581c01 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer, PluginInitializerContext } from 'src/core/public'; +import { Plugin, StartDeps } from './plugin'; +export { StartDeps }; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => { + return new Plugin(initializerContext); +}; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts new file mode 100644 index 0000000000000..348ba215930b0 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/plugin.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { ExpressionsStart } from './types'; +import { setExpressions } from './services'; + +export interface StartDeps { + expressions: ExpressionsStart; +} + +export class Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup({ application }: CoreSetup) { + application.register({ + id: 'kbn_tp_run_pipeline', + title: 'Run Pipeline', + async mount(context, params) { + const { renderApp } = await import('./app/app'); + return renderApp(context, params); + }, + }); + } + + public start(start: CoreStart, { expressions }: StartDeps) { + setExpressions(expressions); + } +} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts new file mode 100644 index 0000000000000..657d8d5150c3a --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public/core'; +import { ExpressionsStart } from './types'; + +export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts new file mode 100644 index 0000000000000..082bb47d80066 --- /dev/null +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ExpressionsStart, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, +} from 'src/plugins/expressions/public'; + +import { Adapters } from 'src/plugins/inspector/public'; + +export { + ExpressionsStart, + Context, + ExpressionRenderHandler, + ExpressionDataHandler, + RenderResult, + Adapters, +}; diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.js b/test/interpreter_functional/test_suites/run_pipeline/basic.ts similarity index 69% rename from test/interpreter_functional/test_suites/run_pipeline/basic.js rename to test/interpreter_functional/test_suites/run_pipeline/basic.ts index 893a79956093c..77853b0bcd6a4 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.js +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.ts @@ -18,13 +18,16 @@ */ import expect from '@kbn/expect'; -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -// this file showcases how to use testing utilities defined in helpers.js together with the kbn_tp_run_pipeline +// this file showcases how to use testing utilities defined in helpers.ts together with the kbn_tp_run_pipeline // test plugin to write autmated tests for interprete -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('basic visualize loader pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); @@ -39,7 +42,12 @@ export default function ({ getService, updateBaselines }) { }); it('correctly sets timeRange', async () => { - const result = await expectExpression('correctly_sets_timerange', 'kibana', {}, { timeRange: 'test' }).getResponse(); + const result = await expectExpression( + 'correctly_sets_timerange', + 'kibana', + {}, + { timeRange: 'test' } + ).getResponse(); expect(result).to.have.property('timeRange', 'test'); }); }); @@ -60,30 +68,32 @@ export default function ({ getService, updateBaselines }) { // we can also do snapshot comparison of result of our expression // to update the snapshots run the tests with --updateBaselines - it ('runs the expression and compares final output', async () => { + it('runs the expression and compares final output', async () => { await expectExpression('final_output_test', expression).toMatchSnapshot(); }); // its also possible to check snapshot at every step of expression (after execution of each function) - it ('runs the expression and compares output at every step', async () => { + it('runs the expression and compares output at every step', async () => { await expectExpression('step_output_test', expression).steps.toMatchSnapshot(); }); // and we can do screenshot comparison of the rendered output of expression (if expression returns renderable) - it ('runs the expression and compares screenshots', async () => { + it('runs the expression and compares screenshots', async () => { await expectExpression('final_screenshot_test', expression).toMatchScreenshot(); }); // it is also possible to combine different checks - it ('runs the expression and combines different checks', async () => { - await (await expectExpression('combined_test', expression).steps.toMatchSnapshot()).toMatchScreenshot(); + it('runs the expression and combines different checks', async () => { + await ( + await expectExpression('combined_test', expression).steps.toMatchSnapshot() + ).toMatchScreenshot(); }); }); // if we want to do multiple different tests using the same data, or reusing a part of expression its // possible to retrieve the intermediate result and reuse it in later expressions describe('reusing partial results', () => { - it ('does some screenshot comparisons', async () => { + it('does some screenshot comparisons', async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, {"id":"2","enabled":true,"type":"terms","schema":"segment","params": @@ -93,17 +103,20 @@ export default function ({ getService, updateBaselines }) { const context = await expectExpression('partial_test', expression).getResponse(); // we reuse that response to render 3 different charts and compare screenshots with baselines - const tagCloudExpr = - `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; - await (await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const tagCloudExpr = `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; + await ( + await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); - const metricExpr = - `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; - await (await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const metricExpr = `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; + await ( + await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); - const regionMapExpr = - `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; - await (await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot()).toMatchScreenshot(); + const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; + await ( + await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.js b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts similarity index 55% rename from test/interpreter_functional/test_suites/run_pipeline/helpers.js rename to test/interpreter_functional/test_suites/run_pipeline/helpers.ts index 4df86d3418f1f..e1ec18fae5e3a 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.js +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -18,14 +18,45 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; +import { + ExpressionDataHandler, + RenderResult, + Context, +} from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types'; + +type UnWrapPromise = T extends Promise ? U : T; +export type ExpressionResult = UnWrapPromise>; + +export type ExpectExpression = ( + name: string, + expression: string, + context?: Context, + initialContext?: Context +) => ExpectExpressionHandler; + +export interface ExpectExpressionHandler { + toReturn: (expectedResult: ExpressionResult) => Promise; + getResponse: () => Promise; + runExpression: (step?: string, stepContext?: Context) => Promise; + steps: { + toMatchSnapshot: () => Promise; + }; + toMatchSnapshot: () => Promise; + toMatchScreenshot: () => Promise; +} // helper for testing interpreter expressions -export const expectExpressionProvider = ({ getService, updateBaselines }) => { +export function expectExpressionProvider({ + getService, + updateBaselines, +}: Pick & { updateBaselines: boolean }): ExpectExpression { const browser = getService('browser'); const screenshot = getService('screenshots'); const snapshots = getService('snapshots'); const log = getService('log'); const testSubjects = getService('testSubjects'); + /** * returns a handler object to test a given expression * @name: name of the test @@ -34,20 +65,25 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @initialContext: initialContext provided to the expression * @returns handler object */ - return (name, expression, context = {}, initialContext = {}) => { + return ( + name: string, + expression: string, + context: Context = {}, + initialContext: Context = {} + ): ExpectExpressionHandler => { log.debug(`executing expression ${expression}`); const steps = expression.split('|'); // todo: we should actually use interpreter parser and get the ast - let responsePromise; + let responsePromise: Promise; - const handler = { + const handler: ExpectExpressionHandler = { /** * checks if provided object matches expression result * @param result: expected expression result * @returns {Promise} */ - toReturn: async result => { + toReturn: async (expectedResult: ExpressionResult) => { const pipelineResponse = await handler.getResponse(); - expect(pipelineResponse).to.eql(result); + expect(pipelineResponse).to.eql(expectedResult); }, /** * returns expression response @@ -63,16 +99,31 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @param stepContext: context to provide to expression * @returns {Promise<*>} result of running expression */ - runExpression: async (step, stepContext) => { + runExpression: async ( + step: string = expression, + stepContext: Context = context + ): Promise => { log.debug(`running expression ${step || expression}`); - const promise = browser.executeAsync((expression, context, initialContext, done) => { - if (!context) context = {}; - if (!context.type) context.type = 'null'; - window.runPipeline(expression, context, initialContext).then(result => { - done(result); - }); - }, step || expression, stepContext || context, initialContext); - return await promise; + return browser.executeAsync( + ( + _expression: string, + _currentContext: Context & { type: string }, + _initialContext: Context, + done: (expressionResult: ExpressionResult) => void + ) => { + if (!_currentContext) _currentContext = { type: 'null' }; + if (!_currentContext.type) _currentContext.type = 'null'; + return window + .runPipeline(_expression, _currentContext, _initialContext) + .then(expressionResult => { + done(expressionResult); + return expressionResult; + }); + }, + step, + stepContext, + initialContext + ); }, steps: { /** @@ -80,17 +131,19 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { * @returns {Promise} */ toMatchSnapshot: async () => { - let lastResponse; + let lastResponse: ExpressionResult; for (let i = 0; i < steps.length; i++) { const step = steps[i]; - lastResponse = await handler.runExpression(step, lastResponse); - const diff = await snapshots.compareAgainstBaseline(name + i, toSerializable(lastResponse), updateBaselines); + lastResponse = await handler.runExpression(step, lastResponse!); + const diff = await snapshots.compareAgainstBaseline( + name + i, + toSerializable(lastResponse!), + updateBaselines + ); expect(diff).to.be.lessThan(0.05); } if (!responsePromise) { - responsePromise = new Promise(resolve => { - resolve(lastResponse); - }); + responsePromise = Promise.resolve(lastResponse!); } return handler; }, @@ -101,7 +154,11 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { */ toMatchSnapshot: async () => { const pipelineResponse = await handler.getResponse(); - await snapshots.compareAgainstBaseline(name, toSerializable(pipelineResponse), updateBaselines); + await snapshots.compareAgainstBaseline( + name, + toSerializable(pipelineResponse), + updateBaselines + ); return handler; }, /** @@ -111,24 +168,31 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { toMatchScreenshot: async () => { const pipelineResponse = await handler.getResponse(); log.debug('starting to render'); - const result = await browser.executeAsync((context, done) => { - window.renderPipelineResponse(context).then(result => { - done(result); - }); - }, pipelineResponse); + const result = await browser.executeAsync( + (_context: ExpressionResult, done: (renderResult: RenderResult) => void) => + window.renderPipelineResponse(_context).then(renderResult => { + done(renderResult); + return renderResult; + }), + pipelineResponse + ); log.debug('response of rendering: ', result); const chartEl = await testSubjects.find('pluginChart'); - const percentDifference = await screenshot.compareAgainstBaseline(name, updateBaselines, chartEl); + const percentDifference = await screenshot.compareAgainstBaseline( + name, + updateBaselines, + chartEl + ); expect(percentDifference).to.be.lessThan(0.1); return handler; - } + }, }; return handler; }; - function toSerializable(response) { + function toSerializable(response: ExpressionResult) { if (response.error) { // in case of error, pass through only message to the snapshot // as error could be expected and stack trace shouldn't be part of the snapshot @@ -136,4 +200,4 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => { } return response; } -}; +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.js b/test/interpreter_functional/test_suites/run_pipeline/index.ts similarity index 82% rename from test/interpreter_functional/test_suites/run_pipeline/index.js rename to test/interpreter_functional/test_suites/run_pipeline/index.ts index 3c1ce2314f55f..031a0e3576ccc 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.js +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -17,7 +17,9 @@ * under the License. */ -export default function ({ getService, getPageObjects, loadTestFile }) { +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); @@ -25,13 +27,16 @@ export default function ({ getService, getPageObjects, loadTestFile }) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'header']); - describe('runPipeline', function () { + describe('runPipeline', function() { this.tags(['skipFirefox']); before(async () => { await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/logstash_functional'); await esArchiver.load('../functional/fixtures/es_archiver/visualize_embedding'); - await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', 'defaultIndex': 'logstash-*' }); + await kibanaServer.uiSettings.replace({ + 'dateFormat:tz': 'Australia/North', + defaultIndex: 'logstash-*', + }); await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('settings'); await appsMenu.clickLink('Run Pipeline'); diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.js b/test/interpreter_functional/test_suites/run_pipeline/metric.ts similarity index 64% rename from test/interpreter_functional/test_suites/run_pipeline/metric.js rename to test/interpreter_functional/test_suites/run_pipeline/metric.ts index 78d571b3583be..c238bedfa28ce 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.js +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.ts @@ -17,18 +17,21 @@ * under the License. */ -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('metricVis pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); describe('correctly renders metric', () => { - let dataContext; + let dataContext: ExpressionResult; before(async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, @@ -44,27 +47,46 @@ export default function ({ getService, updateBaselines }) { it('with invalid data', async () => { const expression = 'metricVis metric={visdimension 0}'; - await (await expectExpression('metric_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('metric_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with single metric data', async () => { const expression = 'metricVis metric={visdimension 0}'; - await (await expectExpression('metric_single_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression( + 'metric_single_metric_data', + expression, + dataContext + ).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with multiple metric data', async () => { const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}'; - await (await expectExpression('metric_multi_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression( + 'metric_multi_metric_data', + expression, + dataContext + ).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with metric and bucket data', async () => { const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}'; - await (await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with percentage option', async () => { - const expression = 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; - await (await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; + await ( + await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts similarity index 61% rename from test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js rename to test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts index 7c0e2d7190703..2451df4db6310 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.js +++ b/test/interpreter_functional/test_suites/run_pipeline/tag_cloud.ts @@ -17,18 +17,21 @@ * under the License. */ -import { expectExpressionProvider } from './helpers'; +import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, updateBaselines }) { - - let expectExpression; +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; describe('tag cloud pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); describe('correctly renders tagcloud', () => { - let dataContext; + let dataContext: ExpressionResult; before(async () => { const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, @@ -41,27 +44,39 @@ export default function ({ getService, updateBaselines }) { it('with invalid data', async () => { const expression = 'tagcloud metric={visdimension 0}'; - await (await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with just metric data', async () => { const expression = 'tagcloud metric={visdimension 0}'; - await (await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with metric and bucket data', async () => { const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1}'; - await (await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + await ( + await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with font size options', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40'; - await (await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40'; + await ( + await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); it('with scale and orientation options', async () => { - const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"'; - await (await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + const expression = + 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"'; + await ( + await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot() + ).toMatchScreenshot(); }); }); }); From 5899aa5a8c409e88e03c3f67b0580cabc21265da Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 26 Nov 2019 14:23:10 +0100 Subject: [PATCH 083/128] Calculate Console app height (#51707) --- src/legacy/core_plugins/console/public/quarantined/_app.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/legacy/core_plugins/console/public/quarantined/_app.scss b/src/legacy/core_plugins/console/public/quarantined/_app.scss index 1e13b6b483981..b19fd438f8ee3 100644 --- a/src/legacy/core_plugins/console/public/quarantined/_app.scss +++ b/src/legacy/core_plugins/console/public/quarantined/_app.scss @@ -1,5 +1,8 @@ // TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules). +@import '@elastic/eui/src/components/header/variables'; + #consoleRoot { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); display: flex; flex: 1 1 auto; // Make sure the editor actions don't create scrollbars on this container From ac0e3e12e2fcdaafe74acfb27044471f3ffd4dce Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 26 Nov 2019 14:24:06 +0100 Subject: [PATCH 084/128] [SearchProfiler] Copy updates (#51700) * Update search profiler copy * Fix pristine logic * Update searchprofiler styles --- .../components/empty_tree_placeholder.tsx | 5 ++--- .../profile_loading_placeholder.tsx | 2 +- .../application/containers/main/main.tsx | 2 +- .../np_ready/application/store/reducer.ts | 7 +------ .../np_ready/application/store/store.ts | 2 +- .../np_ready/application/styles/_index.scss | 21 +++---------------- .../styles/components/_profile_tree.scss | 4 ---- 7 files changed, 9 insertions(+), 34 deletions(-) diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx index bf27620dcac18..d709a8feb48bd 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/empty_tree_placeholder.tsx @@ -15,13 +15,12 @@ export const EmptyTreePlaceHolder = () => { {/* TODO: translations */}

    {i18n.translate('xpack.searchProfiler.emptyProfileTreeTitle', { - defaultMessage: 'Nothing to see here yet.', + defaultMessage: 'No queries to profile', })}

    {i18n.translate('xpack.searchProfiler.emptyProfileTreeDescription', { - defaultMessage: - 'Enter a query and press the "Profile" button or provide profile data in the editor.', + defaultMessage: 'Enter a query, click Profile, and see the results here.', })}

    diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx index fb09c6cddf70a..a7db54b670a84 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/components/profile_loading_placeholder.tsx @@ -13,7 +13,7 @@ export const ProfileLoadingPlaceholder = () => {

    {i18n.translate('xpack.searchProfiler.profilingLoaderText', { - defaultMessage: 'Profiling...', + defaultMessage: 'Loading query profiles...', })}

    diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx index 7f5d223949e61..63ae5c7583625 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/containers/main/main.tsx @@ -93,7 +93,7 @@ export const Main = () => { return ( <> - + {renderLicenseWarning()} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts index dac9dab9bd092..615511786afd1 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/reducer.ts @@ -12,7 +12,6 @@ import { OnHighlightChangeArgs } from '../components/profile_tree'; import { ShardSerialized, Targets } from '../types'; export type Action = - | { type: 'setPristine'; value: boolean } | { type: 'setProfiling'; value: boolean } | { type: 'setHighlightDetails'; value: OnHighlightChangeArgs | null } | { type: 'setActiveTab'; value: Targets | null } @@ -20,12 +19,8 @@ export type Action = export const reducer: Reducer = (state, action) => produce(state, draft => { - if (action.type === 'setPristine') { - draft.pristine = action.value; - return; - } - if (action.type === 'setProfiling') { + draft.pristine = false; draft.profiling = action.value; if (draft.profiling) { draft.currentResponse = null; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts index 7b5a1ce93583d..7008854a16285 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/store/store.ts @@ -18,7 +18,7 @@ export interface State { export const initialState: State = { profiling: false, - pristine: false, + pristine: true, highlightDetails: null, activeTab: null, currentResponse: null, diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss index a72d079354f89..d36a587b9257f 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/_index.scss @@ -10,12 +10,6 @@ @import 'containers/main'; @import 'containers/profile_query_editor'; -#searchProfilerAppRoot { - height: 100%; - display: flex; - flex: 1 1 auto; -} - .prfDevTool__licenseWarning { &__container { max-width: 1000px; @@ -55,19 +49,10 @@ } } -.prfDevTool { - height: calc(100vh - #{$euiHeaderChildSize}); +.appRoot { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); overflow: hidden; - - .devApp__container { - height: 100%; - overflow: hidden; - flex-shrink: 1; - } - - &__container { - overflow: hidden; - } + flex-shrink: 1; } .prfDevTool__detail { diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss index cc4d334f58fd3..c7dc4a305acb2 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/styles/components/_profile_tree.scss @@ -5,10 +5,6 @@ $badgeSize: $euiSize * 5.5; .prfDevTool__profileTree { - &__container { - height: 100%; - } - &__shardDetails--dim small { color: $euiColorDarkShade; } From 07bc6907776a4c94c072b6934617ad6a2ee477a7 Mon Sep 17 00:00:00 2001 From: ffknob Date: Tue, 26 Nov 2019 10:29:56 -0300 Subject: [PATCH 085/128] [SR] Prevents negative values for Snapshot retention policies (#51295) --- .../client_integration/policy_add.test.ts | 21 +++++++++++++ .../policy_form/steps/step_retention.tsx | 6 +++- .../services/validation/validate_policy.ts | 30 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index 2d85a61b04852..bc48d6d6312fb 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -18,6 +18,8 @@ jest.mock('ui/i18n', () => { return { I18nContext }; }); +jest.mock('ui/new_platform'); + const POLICY_NAME = 'my_policy'; const SNAPSHOT_NAME = 'my_snapshot'; const MIN_COUNT = '5'; @@ -141,6 +143,25 @@ describe.skip('', () => { 'Minimum count cannot be greater than maximum count.', ]); }); + + test('should not allow negative values for the delete after, minimum and maximum counts', () => { + const { find, form } = testBed; + + form.setInputValue('expireAfterValueInput', '-1'); + find('expireAfterValueInput').simulate('blur'); + + form.setInputValue('minCountInput', '-1'); + find('minCountInput').simulate('blur'); + + form.setInputValue('maxCountInput', '-1'); + find('maxCountInput').simulate('blur'); + + expect(form.getErrorsMessages()).toEqual([ + 'Delete after cannot be negative.', + 'Minimum count cannot be negative.', + 'Maximum count cannot be negative.', + ]); + }); }); }); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx index c88cbd2736df6..df7e2c8807d9f 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx @@ -85,7 +85,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ } describedByIds={['expirationDescription']} isInvalid={touched.expireAfterValue && Boolean(errors.expireAfterValue)} - error={errors.expireAfter} + error={errors.expireAfterValue} fullWidth > @@ -100,6 +100,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="expireAfterValueInput" + min={0} /> @@ -167,6 +168,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="minCountInput" + min={0} /> @@ -179,6 +181,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ /> } describedByIds={['countDescription']} + isInvalid={touched.maxCount && Boolean(errors.maxCount)} error={errors.maxCount} fullWidth > @@ -193,6 +196,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }); }} data-test-subj="maxCountInput" + min={0} /> diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts index 80734d2f0522c..3f27da82bf56d 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts @@ -28,7 +28,9 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { schedule: [], repository: [], indices: [], + expireAfterValue: [], minCount: [], + maxCount: [], }, }; @@ -92,6 +94,34 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { }) ); } + + if (retention && retention.expireAfterValue && retention.expireAfterValue < 0) { + validation.errors.expireAfterValue.push( + i18n.translate( + 'xpack.snapshotRestore.policyValidation.invalidNegativeDeleteAfterErrorMessage', + { + defaultMessage: 'Delete after cannot be negative.', + } + ) + ); + } + + if (retention && retention.minCount && retention.minCount < 0) { + validation.errors.minCount.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidNegativeMinCountErrorMessage', { + defaultMessage: 'Minimum count cannot be negative.', + }) + ); + } + + if (retention && retention.maxCount && retention.maxCount < 0) { + validation.errors.maxCount.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidNegativeMaxCountErrorMessage', { + defaultMessage: 'Maximum count cannot be negative.', + }) + ); + } + // Remove fields with no errors validation.errors = Object.entries(validation.errors) .filter(([key, value]) => value.length > 0) From 75d261d48cd1d777656f3b8ec46ba45019680b2c Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 26 Nov 2019 15:49:53 +0200 Subject: [PATCH 086/128] =?UTF-8?q?Move=20IndexPatternsSelector=20?= =?UTF-8?q?=E2=87=92=20NP=20(#51620)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move IndexPatternsSelector to NP Replace import from ui/index_patterns with new platform imports * karma mock * added mock * Fix jest tests --- src/legacy/core_plugins/data/public/index.ts | 1 - .../index_patterns_service.mock.ts | 1 - .../index_patterns/index_patterns_service.ts | 5 +-- .../data/public/index_patterns/utils.ts | 13 -------- .../components/editor/controls_tab.test.js | 23 +++++++++++-- .../editor/index_pattern_select_form_row.js | 5 +-- .../editor/list_control_editor.test.js | 16 +++++++-- .../editor/range_control_editor.test.js | 21 +++++++++--- .../public/index_patterns/__mocks__/index.ts | 1 - src/legacy/ui/public/index_patterns/index.ts | 1 - .../new_platform/new_platform.karma_mock.js | 7 ++++ .../lib/get_index_pattern_title.ts | 33 +++++++++++++++++++ .../data/public/index_patterns/lib/index.ts | 20 +++++++++++ src/plugins/data/public/mocks.ts | 3 ++ src/plugins/data/public/plugin.ts | 4 +++ src/plugins/data/public/types.ts | 4 +++ src/plugins/data/public/ui/index.ts | 1 + .../public/ui/index_pattern_select}/index.ts | 0 .../index_pattern_select.tsx | 6 ++-- .../join_editor/resources/join_expression.js | 4 ++- .../create_source_editor.js | 4 ++- .../es_pew_pew_source/create_source_editor.js | 3 +- .../es_search_source/create_source_editor.js | 4 ++- 23 files changed, 141 insertions(+), 39 deletions(-) create mode 100644 src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts create mode 100644 src/plugins/data/public/index_patterns/lib/index.ts rename src/{legacy/core_plugins/data/public/index_patterns/components => plugins/data/public/ui/index_pattern_select}/index.ts (100%) rename src/{legacy/core_plugins/data/public/index_patterns/components => plugins/data/public/ui/index_pattern_select}/index_pattern_select.tsx (97%) diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index c1b4226e6e49f..1349187779061 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -47,7 +47,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - IndexPatternSelect, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts index 5dcf4005ef4e8..db1ece78e7b4d 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.mock.ts @@ -33,7 +33,6 @@ const createSetupContractMock = () => { flattenHitWrapper: jest.fn().mockImplementation(flattenHitWrapper), formatHitProvider: jest.fn(), indexPatterns: jest.fn() as any, - IndexPatternSelect: jest.fn(), __LEGACY: { // For BWC we must temporarily export the class implementation of Field, // which is only used externally by the Index Pattern UI. diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index f97246bc5a9bf..381cd491f0210 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -25,7 +25,6 @@ import { } from 'src/core/public'; import { FieldFormatsStart } from '../../../../../plugins/data/public'; import { Field, FieldList, FieldListInterface, FieldType } from './fields'; -import { createIndexPatternSelect } from './components'; import { setNotifications, setFieldFormats } from './services'; import { @@ -79,7 +78,6 @@ export class IndexPatternsService { return { ...this.setupApi, indexPatterns: new IndexPatterns(uiSettings, savedObjectsClient, http), - IndexPatternSelect: createIndexPatternSelect(savedObjectsClient), }; } @@ -91,7 +89,6 @@ export class IndexPatternsService { // static code /** @public */ -export { IndexPatternSelect } from './components'; export { CONTAINS_SPACES, getFromSavedObject, @@ -120,4 +117,4 @@ export type IndexPatternsStart = ReturnType; export { IndexPattern, IndexPatterns, StaticIndexPattern, Field, FieldType, FieldListInterface }; /** @public */ -export { getIndexPatternTitle, findIndexPatternByTitle } from './utils'; +export { findIndexPatternByTitle } from './utils'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.ts b/src/legacy/core_plugins/data/public/index_patterns/utils.ts index 8542c1dcce24d..8c2878a3ff9ba 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/utils.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/utils.ts @@ -71,19 +71,6 @@ export async function findIndexPatternByTitle( ); } -export async function getIndexPatternTitle( - client: SavedObjectsClientContract, - indexPatternId: string -): Promise> { - const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; - - if (savedObject.error) { - throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); - } - - return savedObject.attributes.title; -} - function indexPatternContainsSpaces(indexPattern: string): boolean { return indexPattern.includes(' '); } diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js index 27f37421b0e25..45981adf9af45 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js @@ -17,8 +17,27 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); +jest.mock('../../../../../core_plugins/data/public/legacy', () => ({ + indexPatterns: { + indexPatterns: { + get: jest.fn(), + } + } +})); + +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
    ; + } + } + } + }, + }, +})); import React from 'react'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js index 663a36ab69f46..c48123f3db714 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js @@ -20,12 +20,13 @@ import PropTypes from 'prop-types'; import React from 'react'; import { injectI18n } from '@kbn/i18n/react'; -import { IndexPatternSelect } from 'ui/index_patterns'; - import { EuiFormRow, } from '@elastic/eui'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function IndexPatternSelectFormRowUi(props) { const { controlIndex, diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js index ea029af9e4890..b37e8af0895fe 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js @@ -17,12 +17,24 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
    ; + } + } + } + }, + }, +})); import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; + import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js index 5a698d65286ac..8d601f5a727d1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js @@ -17,19 +17,30 @@ * under the License. */ -jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +jest.mock('ui/new_platform', () => ({ + npStart: { + plugins: { + data: { + ui: { + IndexPatternSelect: () => { + return
    ; + } + } + } + }, + }, +})); + import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; -import { - RangeControlEditor, -} from './range_control_editor'; +import { RangeControlEditor } from './range_control_editor'; const controlParams = { id: '1', diff --git a/src/legacy/ui/public/index_patterns/__mocks__/index.ts b/src/legacy/ui/public/index_patterns/__mocks__/index.ts index f51ae86b5c9a7..145045a90ade8 100644 --- a/src/legacy/ui/public/index_patterns/__mocks__/index.ts +++ b/src/legacy/ui/public/index_patterns/__mocks__/index.ts @@ -35,7 +35,6 @@ export { CONTAINS_SPACES, getFromSavedObject, getRoutes, - IndexPatternSelect, validateIndexPattern, ILLEGAL_CHARACTERS, INDEX_PATTERN_ILLEGAL_CHARACTERS, diff --git a/src/legacy/ui/public/index_patterns/index.ts b/src/legacy/ui/public/index_patterns/index.ts index 690a9cffaa138..d0ff0aaa8c72c 100644 --- a/src/legacy/ui/public/index_patterns/index.ts +++ b/src/legacy/ui/public/index_patterns/index.ts @@ -30,7 +30,6 @@ export const { FieldList, // only used in Discover and StubIndexPattern flattenHitWrapper, formatHitProvider, - IndexPatternSelect, // only used in x-pack/plugin/maps and input control vis } = data.indexPatterns; // static code diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 773d4283cad88..ff89ef69d53ca 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -26,6 +26,10 @@ const mockObservable = () => { }; }; +const mockComponent = () => { + return null; +}; + export const mockUiSettings = { get: (item) => { return mockUiSettings[item]; @@ -139,6 +143,9 @@ export const npStart = { getProvider: sinon.fake(), }, getSuggestions: sinon.fake(), + ui: { + IndexPatternSelect: mockComponent, + }, query: { filterManager: { getFetches$: sinon.fake(), diff --git a/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts b/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts new file mode 100644 index 0000000000000..777a12c7e2884 --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/get_index_pattern_title.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; + +export async function getIndexPatternTitle( + client: SavedObjectsClientContract, + indexPatternId: string +): Promise> { + const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject; + + if (savedObject.error) { + throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`); + } + + return savedObject.attributes.title; +} diff --git a/src/plugins/data/public/index_patterns/lib/index.ts b/src/plugins/data/public/index_patterns/lib/index.ts new file mode 100644 index 0000000000000..d1c229513aa33 --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getIndexPatternTitle } from './get_index_pattern_title'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index ff5c96c2d89ed..ceb57b4a3a564 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -66,6 +66,9 @@ const createStartContract = (): Start => { search: { search: jest.fn() }, fieldFormats: fieldFormatsMock as FieldFormatsStart, query: queryStartMock, + ui: { + IndexPatternSelect: jest.fn(), + }, }; return startContract; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 3aa9cd9a0bcb4..d8c45b6786c0c 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -25,6 +25,7 @@ import { getSuggestionsProvider } from './suggestions_provider'; import { SearchService } from './search/search_service'; import { FieldFormatsService } from './field_formats_provider'; import { QueryService } from './query'; +import { createIndexPatternSelect } from './ui/index_pattern_select'; export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteProviderRegister(); @@ -59,6 +60,9 @@ export class DataPublicPlugin implements Plugin; + }; } export * from './autocomplete_provider/types'; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 79107d1ede676..cb7c92b00ea3a 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,5 +17,6 @@ * under the License. */ +export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { applyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index.ts b/src/plugins/data/public/ui/index_pattern_select/index.ts similarity index 100% rename from src/legacy/core_plugins/data/public/index_patterns/components/index.ts rename to src/plugins/data/public/ui/index_pattern_select/index.ts diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx rename to src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 77692d7bcaa0d..f868e4b1f7504 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -21,10 +21,10 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { EuiComboBox } from '@elastic/eui'; -import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../../core/public'; -import { getIndexPatternTitle } from '../utils'; +import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; +import { getIndexPatternTitle } from '../../index_patterns/lib'; -interface IndexPatternSelectProps { +export interface IndexPatternSelectProps { onChange: (opt: any) => void; indexPatternId: string; placeholder: string; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 44629d16e6fb3..01c323d73f19e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -16,7 +16,6 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; @@ -25,6 +24,9 @@ import { indexPatternService, } from '../../../../kibana_services'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + export class JoinExpression extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js index 395b6ac5cc431..3d02b075b3b81 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { RENDER_AS } from './render_as'; import { indexPatternService } from '../../../kibana_services'; @@ -22,6 +21,9 @@ import { } from '@elastic/eui'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function filterGeoField({ type }) { return [ES_GEO_FIELD_TYPE.GEO_POINT].includes(type); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js index 9f9789374274a..897ded43be28b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; @@ -20,6 +19,8 @@ import { } from '@elastic/eui'; import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; const GEO_FIELD_TYPES = [ES_GEO_FIELD_TYPE.GEO_POINT]; function filterGeoField({ type }) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js index 61300ed209c1f..a6ba31366d504 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js @@ -9,7 +9,6 @@ import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, EuiSpacer, EuiSwitch, EuiCallOut } from '@elastic/eui'; -import { IndexPatternSelect } from 'ui/index_patterns'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { indexPatternService } from '../../../kibana_services'; import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout'; @@ -19,6 +18,9 @@ import { kfetch } from 'ui/kfetch'; import { ES_GEO_FIELD_TYPE, GIS_API_PATH, ES_SIZE_LIMIT } from '../../../../common/constants'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; +import { npStart } from 'ui/new_platform'; +const { IndexPatternSelect } = npStart.plugins.data.ui; + function filterGeoField(field) { return [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE].includes(field.type); } From 61b3972e5dea889b7cfb4622c89db13c0e3923d5 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 26 Nov 2019 08:01:20 -0700 Subject: [PATCH 087/128] [Maps] refactor static can skip logic from layer class (#51627) --- .../plugins/maps/public/layers/layer.js | 41 --- .../plugins/maps/public/layers/layer.test.js | 113 ------- .../maps/public/layers/util/can_skip_fetch.js | 130 ++++++++ .../public/layers/util/can_skip_fetch.test.js | 287 ++++++++++++++++++ .../maps/public/layers/vector_layer.js | 111 ++----- .../maps/public/layers/vector_layer.test.js | 185 ----------- 6 files changed, 436 insertions(+), 431 deletions(-) delete mode 100644 x-pack/legacy/plugins/maps/public/layers/layer.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 72a89046ed2f5..1c2f33df66bf8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -6,8 +6,6 @@ import _ from 'lodash'; import React from 'react'; import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import turf from 'turf'; -import turfBooleanContains from '@turf/boolean-contains'; import { DataRequest } from './util/data_request'; import { MAX_ZOOM, @@ -19,9 +17,6 @@ import uuid from 'uuid/v4'; import { copyPersistentState } from '../reducers/util'; import { i18n } from '@kbn/i18n'; -const SOURCE_UPDATE_REQUIRED = true; -const NO_SOURCE_UPDATE_REQUIRED = false; - export class AbstractLayer { constructor({ layerDescriptor, source }) { @@ -316,42 +311,7 @@ export class AbstractLayer { throw new Error('Should implement AbstractLayer#syncLayerWithMB'); } - updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { - const extentAware = source.isFilterByMapBounds(); - if (!extentAware) { - return NO_SOURCE_UPDATE_REQUIRED; - } - const { buffer: previousBuffer } = prevMeta; - const { buffer: newBuffer } = nextMeta; - - if (!previousBuffer) { - return SOURCE_UPDATE_REQUIRED; - } - - if (_.isEqual(previousBuffer, newBuffer)) { - return NO_SOURCE_UPDATE_REQUIRED; - } - - const previousBufferGeometry = turf.bboxPolygon([ - previousBuffer.minLon, - previousBuffer.minLat, - previousBuffer.maxLon, - previousBuffer.maxLat - ]); - const newBufferGeometry = turf.bboxPolygon([ - newBuffer.minLon, - newBuffer.minLat, - newBuffer.maxLon, - newBuffer.maxLat - ]); - const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); - - const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); - return doesPreviousBufferContainNewBuffer && !isTrimmed - ? NO_SOURCE_UPDATE_REQUIRED - : SOURCE_UPDATE_REQUIRED; - } getLayerTypeIconName() { throw new Error('should implement Layer#getLayerTypeIconName'); @@ -407,4 +367,3 @@ export class AbstractLayer { } } - diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.test.js b/x-pack/legacy/plugins/maps/public/layers/layer.test.js deleted file mode 100644 index 98be0855cd4b7..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/layer.test.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AbstractLayer } from './layer'; - -describe('layer', () => { - const layer = new AbstractLayer({ layerDescriptor: {} }); - - describe('updateDueToExtent', () => { - - it('should be false when the source is not extent aware', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return false; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when buffers are the same', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when the new buffer is contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent( - sourceMock, - { buffer: oldBuffer, areResultsTrimmed: true }, - { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when meta has no old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when the new buffer is not contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 7.5, - maxLon: 92.5, - minLat: -2.5, - minLon: 82.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js new file mode 100644 index 0000000000000..610c704b34ec6 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; +import turf from 'turf'; +import turfBooleanContains from '@turf/boolean-contains'; +import { isRefreshOnlyQuery } from './is_refresh_only_query'; + +const SOURCE_UPDATE_REQUIRED = true; +const NO_SOURCE_UPDATE_REQUIRED = false; + +export function updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { + const extentAware = source.isFilterByMapBounds(); + if (!extentAware) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const { buffer: previousBuffer } = prevMeta; + const { buffer: newBuffer } = nextMeta; + + if (!previousBuffer) { + return SOURCE_UPDATE_REQUIRED; + } + + if (_.isEqual(previousBuffer, newBuffer)) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const previousBufferGeometry = turf.bboxPolygon([ + previousBuffer.minLon, + previousBuffer.minLat, + previousBuffer.maxLon, + previousBuffer.maxLat + ]); + const newBufferGeometry = turf.bboxPolygon([ + newBuffer.minLon, + newBuffer.minLat, + newBuffer.maxLon, + newBuffer.maxLat + ]); + const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); + + const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); + return doesPreviousBufferContainNewBuffer && !isTrimmed + ? NO_SOURCE_UPDATE_REQUIRED + : SOURCE_UPDATE_REQUIRED; +} + +export async function canSkipSourceUpdate({ source, prevDataRequest, nextMeta }) { + + const timeAware = await source.isTimeAware(); + const refreshTimerAware = await source.isRefreshTimerAware(); + const extentAware = source.isFilterByMapBounds(); + const isFieldAware = source.isFieldAware(); + const isQueryAware = source.isQueryAware(); + const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); + + if ( + !timeAware && + !refreshTimerAware && + !extentAware && + !isFieldAware && + !isQueryAware && + !isGeoGridPrecisionAware + ) { + return (prevDataRequest && prevDataRequest.hasDataOrRequestInProgress()); + } + + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + let updateDueToTime = false; + if (timeAware) { + updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); + } + + let updateDueToRefreshTimer = false; + if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { + updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); + } + + let updateDueToFields = false; + if (isFieldAware) { + updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); + } + + let updateDueToQuery = false; + let updateDueToFilters = false; + let updateDueToSourceQuery = false; + let updateDueToApplyGlobalQuery = false; + if (isQueryAware) { + updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; + updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + if (nextMeta.applyGlobalQuery) { + updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); + updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); + } else { + // Global filters and query are not applied to layer search request so no re-fetch required. + // Exception is "Refresh" query. + updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); + } + } + + let updateDueToPrecisionChange = false; + if (isGeoGridPrecisionAware) { + updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); + } + + const updateDueToExtentChange = updateDueToExtent(source, prevMeta, nextMeta); + + const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); + + return !updateDueToTime + && !updateDueToRefreshTimer + && !updateDueToExtentChange + && !updateDueToFields + && !updateDueToQuery + && !updateDueToFilters + && !updateDueToSourceQuery + && !updateDueToApplyGlobalQuery + && !updateDueToPrecisionChange + && !updateDueToSourceMetaChange; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js new file mode 100644 index 0000000000000..77359a6def48f --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { canSkipSourceUpdate, updateDueToExtent } from './can_skip_fetch'; +import { DataRequest } from './data_request'; + +describe('updateDueToExtent', () => { + + it('should be false when the source is not extent aware', async () => { + const sourceMock = { + isFilterByMapBounds: () => { return false; } + }; + expect(updateDueToExtent(sourceMock)).toBe(false); + }); + + describe('source is extent aware', () => { + const sourceMock = { + isFilterByMapBounds: () => { return true; } + }; + + it('should be false when buffers are the same', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })) + .toBe(false); + }); + + it('should be false when the new buffer is contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); + }); + + it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent( + sourceMock, + { buffer: oldBuffer, areResultsTrimmed: true }, + { buffer: newBuffer } + )).toBe(true); + }); + + it('should be true when meta has no old buffer', async () => { + expect(updateDueToExtent(sourceMock)).toBe(true); + }); + + it('should be true when the new buffer is not contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 7.5, + maxLon: 92.5, + minLat: -2.5, + minLon: 82.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(true); + }); + }); +}); + +describe('canSkipSourceUpdate', () => { + const SOURCE_DATA_REQUEST_ID = 'foo'; + + describe('isQueryAware', () => { + + const queryAwareSourceMock = { + isTimeAware: () => { return false; }, + isRefreshTimerAware: () => { return false; }, + isFilterByMapBounds: () => { return false; }, + isFieldAware: () => { return false; }, + isQueryAware: () => { return true; }, + isGeoGridPrecisionAware: () => { return false; }, + }; + const prevFilters = []; + const prevQuery = { + language: 'kuery', + query: 'machine.os.keyword : "win 7"', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' + }; + + describe('applyGlobalQuery is false', () => { + + const prevApplyGlobalQuery = false; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta, + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + + describe('applyGlobalQuery is true', () => { + + const prevApplyGlobalQuery = true; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can not skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 9b553803606ed..57126bb7681b8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -18,10 +18,10 @@ import { } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; -import { isRefreshOnlyQuery } from './util/is_refresh_only_query'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; +import { canSkipSourceUpdate } from './util/can_skip_fetch'; import { assignFeatureIds } from './util/assign_feature_ids'; import { getFillFilterExpression, @@ -229,109 +229,31 @@ export class VectorLayer extends AbstractLayer { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } - async _canSkipSourceUpdate(source, sourceDataId, nextMeta) { - const timeAware = await source.isTimeAware(); - const refreshTimerAware = await source.isRefreshTimerAware(); - const extentAware = source.isFilterByMapBounds(); - const isFieldAware = source.isFieldAware(); - const isQueryAware = source.isQueryAware(); - const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); - - if ( - !timeAware && - !refreshTimerAware && - !extentAware && - !isFieldAware && - !isQueryAware && - !isGeoGridPrecisionAware - ) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - return (sourceDataRequest && sourceDataRequest.hasDataOrRequestInProgress()); - } - - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - if (!sourceDataRequest) { - return false; - } - const prevMeta = sourceDataRequest.getMeta(); - if (!prevMeta) { - return false; - } - - let updateDueToTime = false; - if (timeAware) { - updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); - } - - let updateDueToRefreshTimer = false; - if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { - updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); - } - - let updateDueToFields = false; - if (isFieldAware) { - updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); - } - - let updateDueToQuery = false; - let updateDueToFilters = false; - let updateDueToSourceQuery = false; - let updateDueToApplyGlobalQuery = false; - if (isQueryAware) { - updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; - updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); - if (nextMeta.applyGlobalQuery) { - updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); - updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); - } else { - // Global filters and query are not applied to layer search request so no re-fetch required. - // Exception is "Refresh" query. - updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); - } - } - - let updateDueToPrecisionChange = false; - if (isGeoGridPrecisionAware) { - updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); - } - - const updateDueToExtentChange = this.updateDueToExtent(source, prevMeta, nextMeta); - - const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); - - return !updateDueToTime - && !updateDueToRefreshTimer - && !updateDueToExtentChange - && !updateDueToFields - && !updateDueToQuery - && !updateDueToFilters - && !updateDueToSourceQuery - && !updateDueToApplyGlobalQuery - && !updateDueToPrecisionChange - && !updateDueToSourceMetaChange; - } async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceId(); const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); - const searchFilters = { ...dataFilters, fieldNames: joinSource.getFieldNames(), sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), }; - const canSkip = await this._canSkipSourceUpdate(joinSource, sourceDataId, searchFilters); - if (canSkip) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - const propertiesMap = sourceDataRequest ? sourceDataRequest.getData() : null; + const prevDataRequest = this._findDataRequestForSource(sourceDataId); + + const canSkipFetch = await canSkipSourceUpdate({ + source: joinSource, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { dataHasChanged: false, join: join, - propertiesMap: propertiesMap + propertiesMap: prevDataRequest.getData() }; } @@ -430,12 +352,17 @@ export class VectorLayer extends AbstractLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); const searchFilters = this._getSearchFilters(dataFilters); - const canSkip = await this._canSkipSourceUpdate(this._source, SOURCE_DATA_ID_ORIGIN, searchFilters); - if (canSkip) { - const sourceDataRequest = this.getSourceDataRequest(); + const prevDataRequest = this.getSourceDataRequest(); + + const canSkipFetch = await canSkipSourceUpdate({ + source: this._source, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { refreshed: false, - featureCollection: sourceDataRequest.getData() + featureCollection: prevDataRequest.getData() }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js deleted file mode 100644 index 0a07582c57856..0000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -jest.mock('./joins/inner_join', () => ({ - InnerJoin: Object -})); - -jest.mock('./tooltips/join_tooltip_property', () => ({ - JoinTooltipProperty: Object -})); - -import { VectorLayer } from './vector_layer'; - -describe('_canSkipSourceUpdate', () => { - const SOURCE_DATA_REQUEST_ID = 'foo'; - - describe('isQueryAware', () => { - - const queryAwareSourceMock = { - isTimeAware: () => { return false; }, - isRefreshTimerAware: () => { return false; }, - isFilterByMapBounds: () => { return false; }, - isFieldAware: () => { return false; }, - isQueryAware: () => { return true; }, - isGeoGridPrecisionAware: () => { return false; }, - }; - const prevFilters = []; - const prevQuery = { - language: 'kuery', - query: 'machine.os.keyword : "win 7"', - queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' - }; - - describe('applyGlobalQuery is false', () => { - - const prevApplyGlobalQuery = false; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - - describe('applyGlobalQuery is true', () => { - - const prevApplyGlobalQuery = true; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can not skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - }); -}); From f7d9e7bbf6ddd494ce94575c10238fea12ea0436 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 26 Nov 2019 08:10:25 -0700 Subject: [PATCH 088/128] [SIEM][Detection Engine] Disambiguates signals, rules, alerts, and detection engine by renaming them (#51684) ## Summary * Renames `signals -> rules` when it is specific about rules * Renames `signals -> detection engine` when is generically talking about both rules and signals * Renames `signals -> alerts` in a few spots when it is talking specifically about alerting plugins * Keeps the name of signal when it involves the signals output index or a source input index for potential signals to be generated from * Did a `git mv ` for everything * Updated local variables as well per rules above. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [ ] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- ...ls.js => convert_saved_search_to_rules.js} | 26 +- .../plugins/siem/server/kibana.index.ts | 26 +- .../server/lib/detection_engine/README.md | 18 +- .../alerts/__mocks__/es_results.ts | 8 +- .../{create_signals.ts => create_rules.ts} | 6 +- .../{delete_signals.ts => delete_rules.ts} | 18 +- ...ind_signals.test.ts => find_rules.test.ts} | 4 +- .../alerts/{find_signals.ts => find_rules.ts} | 6 +- .../lib/detection_engine/alerts/get_filter.ts | 6 +- ...ead_signals.test.ts => read_rules.test.ts} | 70 ++-- .../alerts/{read_signals.ts => read_rules.ts} | 50 +-- ...nals_alert_type.ts => rules_alert_type.ts} | 8 +- .../lib/detection_engine/alerts/types.ts | 74 ++-- ...e_signals.test.ts => update_rules.test.ts} | 10 +- .../{update_signals.ts => update_rules.ts} | 40 +- .../lib/detection_engine/alerts/utils.test.ts | 94 ++--- .../lib/detection_engine/alerts/utils.ts | 93 +++-- .../routes/__mocks__/request_responses.ts | 12 +- ...ute.test.ts => create_rules_route.test.ts} | 14 +- ...signals_route.ts => create_rules_route.ts} | 28 +- ...ute.test.ts => delete_rules_route.test.ts} | 18 +- ...signals_route.ts => delete_rules_route.ts} | 18 +- ...route.test.ts => find_rules_route.test.ts} | 14 +- ...d_signals_route.ts => find_rules_route.ts} | 20 +- ...route.test.ts => read_rules_route.test.ts} | 12 +- ...d_signals_route.ts => read_rules_route.ts} | 18 +- .../detection_engine/routes/schemas.test.ts | 343 ++++++++---------- .../lib/detection_engine/routes/schemas.ts | 8 +- ...ute.test.ts => update_rules_route.test.ts} | 16 +- ...signals_route.ts => update_rules_route.ts} | 22 +- .../lib/detection_engine/routes/utils.test.ts | 54 +-- .../lib/detection_engine/routes/utils.ts | 66 ++-- .../lib/detection_engine/scripts/README.md | 7 +- ...ls.sh => convert_saved_search_to_rules.sh} | 2 +- ...e_signal_by_id.sh => delete_rule_by_id.sh} | 2 +- ...y_rule_id.sh => delete_rule_by_rule_id.sh} | 2 +- ...al_by_filter.sh => find_rule_by_filter.sh} | 4 +- .../{find_signals.sh => find_rules.sh} | 2 +- ...ind_signals_sort.sh => find_rules_sort.sh} | 2 +- ...{get_signal_by_id.sh => get_rule_by_id.sh} | 2 +- ...l_by_rule_id.sh => get_rule_by_rule_id.sh} | 2 +- .../scripts/{post_signal.sh => post_rule.sh} | 14 +- .../{post_x_signals.sh => post_x_rules.sh} | 4 +- .../filter_with_empty_query.json | 0 .../filter_without_query.json | 0 .../{signals => rules}/root_or_admin_1.json | 0 .../{signals => rules}/root_or_admin_10.json | 0 .../{signals => rules}/root_or_admin_2.json | 0 .../{signals => rules}/root_or_admin_3.json | 0 .../{signals => rules}/root_or_admin_4.json | 0 .../{signals => rules}/root_or_admin_5.json | 0 .../{signals => rules}/root_or_admin_6.json | 0 .../{signals => rules}/root_or_admin_7.json | 0 .../{signals => rules}/root_or_admin_8.json | 0 .../{signals => rules}/root_or_admin_9.json | 0 .../root_or_admin_filter_9998.json | 0 .../root_or_admin_filter_9999.json | 0 .../root_or_admin_meta.json | 0 .../root_or_admin_saved_query_1.json | 0 .../root_or_admin_saved_query_2.json | 0 .../root_or_admin_saved_query_3.json | 0 .../root_or_admin_update_1.json | 0 .../root_or_admin_update_2.json | 0 .../{signals => rules}/watch_longmont.json | 0 .../{update_signal.sh => update_rule.sh} | 14 +- .../legacy/plugins/siem/server/lib/types.ts | 4 +- 66 files changed, 630 insertions(+), 651 deletions(-) rename x-pack/legacy/plugins/siem/scripts/{convert_saved_search_to_signals.js => convert_saved_search_to_rules.js} (84%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{create_signals.ts => create_rules.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{delete_signals.ts => delete_rules.ts} (65%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{find_signals.test.ts => find_rules.test.ts} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{find_signals.ts => find_rules.ts} (88%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{read_signals.test.ts => read_rules.test.ts} (82%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{read_signals.ts => read_rules.ts} (50%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{signals_alert_type.ts => rules_alert_type.ts} (97%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{update_signals.test.ts => update_rules.test.ts} (80%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{update_signals.ts => update_rules.ts} (66%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{create_signals_route.test.ts => create_rules_route.test.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{create_signals_route.ts => create_rules_route.ts} (72%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{delete_signals_route.test.ts => delete_rules_route.test.ts} (83%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{delete_signals_route.ts => delete_rules_route.ts} (75%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{find_signals_route.test.ts => find_rules_route.test.ts} (89%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{find_signals_route.ts => find_rules_route.ts} (70%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{read_signals_route.test.ts => read_rules_route.test.ts} (88%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{read_signals_route.ts => read_rules_route.ts} (75%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{update_signals_route.test.ts => update_rules_route.test.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{update_signals_route.ts => update_rules_route.ts} (78%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{convert_saved_search_to_signals.sh => convert_saved_search_to_rules.sh} (80%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{delete_signal_by_id.sh => delete_rule_by_id.sh} (91%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{delete_signal_by_rule_id.sh => delete_rule_by_rule_id.sh} (89%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signal_by_filter.sh => find_rule_by_filter.sh} (81%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signals.sh => find_rules.sh} (93%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signals_sort.sh => find_rules_sort.sh} (91%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{get_signal_by_id.sh => get_rule_by_id.sh} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{get_signal_by_rule_id.sh => get_rule_by_rule_id.sh} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{post_signal.sh => post_rule.sh} (68%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{post_x_signals.sh => post_x_rules.sh} (94%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/filter_with_empty_query.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/filter_without_query.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_10.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_3.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_4.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_5.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_6.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_7.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_8.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_9.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_filter_9998.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_filter_9999.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_meta.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_3.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_update_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_update_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/watch_longmont.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{update_signal.sh => update_rule.sh} (67%) diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js similarity index 84% rename from x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js rename to x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js index 263a2a59de31f..3e1c5f51ebb5c 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -11,22 +11,22 @@ const path = require('path'); /* * This script is used to parse a set of saved searches on a file system - * and output signal data compatible json files. + * and output rule data compatible json files. * Example: - * node saved_query_to_signals.js ${HOME}/saved_searches ${HOME}/saved_signals + * node saved_query_to_rules.js ${HOME}/saved_searches ${HOME}/saved_rules * - * After editing any changes in the files of ${HOME}/saved_signals/*.json - * you can then post the signals with a CURL post script such as: + * After editing any changes in the files of ${HOME}/saved_rules/*.json + * you can then post the rules with a CURL post script such as: * - * ./post_signal.sh ${HOME}/saved_signals/*.json + * ./post_rule.sh ${HOME}/saved_rules/*.json * * Note: This script is recursive and but does not preserve folder structure - * when it outputs the saved signals. + * when it outputs the saved rules. */ -// Defaults of the outputted signals since the saved KQL searches do not have +// Defaults of the outputted rules since the saved KQL searches do not have // this type of information. You usually will want to make any hand edits after -// doing a search to KQL conversion before posting it as a signal or checking it +// doing a search to KQL conversion before posting it as a rule or checking it // into another repository. const INTERVAL = '5m'; const SEVERITY = 'low'; @@ -36,8 +36,8 @@ const TO = 'now'; const IMMUTABLE = true; const RISK_SCORE = 50; const ENABLED = false; -let allSignals = ''; -const allSignalsNdJson = 'all_rules.ndjson'; +let allRules = ''; +const allRulesNdJson = 'all_rules.ndjson'; // For converting, if you want to use these instead of rely on the defaults then // comment these in and use them for the script. Otherwise this is commented out @@ -74,7 +74,7 @@ const cleanupFileName = file => { async function main() { if (process.argv.length !== 4) { throw new Error( - 'usage: saved_query_to_signals [input directory with saved searches] [output directory]' + 'usage: saved_query_to_rules [input directory with saved searches] [output directory]' ); } @@ -152,11 +152,11 @@ async function main() { `${outputDir}/${fileToWrite}.json`, JSON.stringify(outputMessage, null, 2) ); - allSignals += `${JSON.stringify(outputMessage)}\n`; + allRules += `${JSON.stringify(outputMessage)}\n`; } } ); - fs.writeFileSync(`${outputDir}/${allSignalsNdJson}`, allSignals); + fs.writeFileSync(`${outputDir}/${allRulesNdJson}`, allRules); } if (require.main === module) { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index a92bca064dab9..2f1530a777042 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -15,13 +15,13 @@ import { timelineSavedObjectType, } from './saved_objects'; -import { signalsAlertType } from './lib/detection_engine/alerts/signals_alert_type'; +import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type'; import { isAlertExecutor } from './lib/detection_engine/alerts/types'; -import { createSignalsRoute } from './lib/detection_engine/routes/create_signals_route'; -import { readSignalsRoute } from './lib/detection_engine/routes/read_signals_route'; -import { findSignalsRoute } from './lib/detection_engine/routes/find_signals_route'; -import { deleteSignalsRoute } from './lib/detection_engine/routes/delete_signals_route'; -import { updateSignalsRoute } from './lib/detection_engine/routes/update_signals_route'; +import { createRulesRoute } from './lib/detection_engine/routes/create_rules_route'; +import { readRulesRoute } from './lib/detection_engine/routes/read_rules_route'; +import { findRulesRoute } from './lib/detection_engine/routes/find_rules_route'; +import { deleteRulesRoute } from './lib/detection_engine/routes/delete_rules_route'; +import { updateRulesRoute } from './lib/detection_engine/routes/update_rules_route'; import { ServerFacade } from './types'; const APP_ID = 'siem'; @@ -33,7 +33,7 @@ export const initServerWithKibana = ( ) => { if (kbnServer.plugins.alerting != null) { const version = kbnServer.config().get('pkg.version'); - const type = signalsAlertType({ logger, version }); + const type = rulesAlertType({ logger, version }); if (isAlertExecutor(type)) { kbnServer.plugins.alerting.setup.registerType(type); } @@ -49,13 +49,13 @@ export const initServerWithKibana = ( kbnServer.config().has('xpack.alerting.enabled') === true ) { logger.info( - 'Detected feature flags for actions and alerting and enabling signals API endpoints' + 'Detected feature flags for actions and alerting and enabling detection engine API endpoints' ); - createSignalsRoute(kbnServer); - readSignalsRoute(kbnServer); - updateSignalsRoute(kbnServer); - deleteSignalsRoute(kbnServer); - findSignalsRoute(kbnServer); + createRulesRoute(kbnServer); + readRulesRoute(kbnServer); + updateRulesRoute(kbnServer); + deleteRulesRoute(kbnServer); + findRulesRoute(kbnServer); } const xpackMainPlugin = kbnServer.plugins.xpack_main; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 5d9d87a1cbc2f..4b1dbf62d0dd4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -24,10 +24,10 @@ xpack.alerting.enabled: true xpack.actions.enabled: true ``` -Start Kibana and you will see these messages indicating signals is activated like so: +Start Kibana and you will see these messages indicating detection engine is activated like so: ```sh -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints ``` If you see crashes like this: @@ -98,10 +98,10 @@ server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status chan server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready ``` -You should also see the SIEM detect the feature flags and start the API endpoints for signals +You should also see the SIEM detect the feature flags and start the API endpoints for detection engine ```sh -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints ``` Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same @@ -125,16 +125,16 @@ which will: - Delete any existing alert tasks you have - Delete any existing signal mapping you might have had. - Add the latest signal index and its mappings using your settings from `SIGNALS_INDEX` environment variable. -- Posts the sample signal from `signals/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable -- The sample signal checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit +- Posts the sample rule from `rules/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable +- The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit Now you can run ```sh -./find_signals.sh +./find_rules.sh ``` -You should see the new signals created like so: +You should see the new rules created like so: ```sh { @@ -184,7 +184,7 @@ Every 5 minutes if you get positive hits you will see messages on info like so: server log [09:54:59.013] [info][plugins][siem] Total signals found from signal rule "id: a556065c-0656-4ba1-ad64-a77ca9d2013b", "ruleId: rule-1": 10000 ``` -Signals are space aware and default to the "default" space for these scripts if you do not export +Rules are space aware and default to the "default" space for these scripts if you do not export the variable of SPACE_URL. For example, if you want to post rules to the space `test-space` you would set your SPACE_URL to be: diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 7d3b51c071c09..079d3658461fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse, AlertTypeParams } from '../types'; +import { SignalSourceHit, SignalSearchResponse, RuleTypeParams } from '../types'; -export const sampleSignalAlertParams = ( +export const sampleRuleAlertParams = ( maxSignals: number | undefined, riskScore?: number | undefined -): AlertTypeParams => ({ +): RuleTypeParams => ({ ruleId: 'rule-1', description: 'Detecting root and admin users', falsePositives: [], @@ -242,4 +242,4 @@ export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearch }, }); -export const sampleSignalId = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; +export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts index 8770282356cf5..7c66714484383 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts @@ -5,9 +5,9 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { SignalParams } from './types'; +import { RuleParams } from './types'; -export const createSignals = async ({ +export const createRules = async ({ alertsClient, actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... description, @@ -33,7 +33,7 @@ export const createSignals = async ({ to, type, references, -}: SignalParams) => { +}: RuleParams) => { return alertsClient.create({ data: { name, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts similarity index 65% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts index d89895772f1ef..c3ca1d79424cf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts @@ -4,27 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { readSignals } from './read_signals'; -import { DeleteSignalParams } from './types'; +import { readRules } from './read_rules'; +import { DeleteRuleParams } from './types'; -export const deleteSignals = async ({ +export const deleteRules = async ({ alertsClient, actionsClient, // TODO: Use this when we have actions such as email, etc... id, ruleId, -}: DeleteSignalParams) => { - const signal = await readSignals({ alertsClient, id, ruleId }); - if (signal == null) { +}: DeleteRuleParams) => { + const rule = await readRules({ alertsClient, id, ruleId }); + if (rule == null) { return null; } if (ruleId != null) { - await alertsClient.delete({ id: signal.id }); - return signal; + await alertsClient.delete({ id: rule.id }); + return rule; } else if (id != null) { try { await alertsClient.delete({ id }); - return signal; + return rule; } catch (err) { if (err.output.statusCode === 404) { return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts index 7873781fb05c4..23f031b22a9dd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getFilter } from './find_signals'; +import { getFilter } from './find_rules'; import { SIGNALS_ID } from '../../../../common/constants'; -describe('find_signals', () => { +describe('find_rules', () => { test('it returns a full filter with an AND if sent down', () => { expect(getFilter('alert.attributes.enabled: true')).toEqual( `alert.attributes.alertTypeId: ${SIGNALS_ID} AND alert.attributes.enabled: true` diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts index 63e6a069c0cfe..c1058bd353e8c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts @@ -5,7 +5,7 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { FindSignalParams } from './types'; +import { FindRuleParams } from './types'; export const getFilter = (filter: string | null | undefined) => { if (filter == null) { @@ -15,7 +15,7 @@ export const getFilter = (filter: string | null | undefined) => { } }; -export const findSignals = async ({ +export const findRules = async ({ alertsClient, perPage, page, @@ -23,7 +23,7 @@ export const findSignals = async ({ filter, sortField, sortOrder, -}: FindSignalParams) => { +}: FindRuleParams) => { return alertsClient.find({ options: { fields, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts index 1aa22ea024cc8..5d3b47ecebfd5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts @@ -5,7 +5,7 @@ */ import { AlertServices } from '../../../../../alerting/server/types'; -import { SignalAlertParams, PartialFilter } from './types'; +import { RuleAlertParams, PartialFilter } from './types'; import { assertUnreachable } from '../../../utils/build_query'; import { Query, @@ -41,7 +41,7 @@ export const getQueryFilter = ( }; interface GetFilterArgs { - type: SignalAlertParams['type']; + type: RuleAlertParams['type']; filter: Record | undefined | null; filters: PartialFilter[] | undefined | null; language: string | undefined | null; @@ -86,7 +86,7 @@ export const getFilter = async ({ if (query != null && language != null && index != null) { return getQueryFilter(query, language, filters || [], index); } else { - // user did not give any additional fall back mechanism for generating a signal + // user did not give any additional fall back mechanism for generating a rule // rethrow error for activity monitoring throw err; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts similarity index 82% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts index 39d1fac8f7a09..b3d7ab1322775 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts @@ -5,7 +5,7 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { readSignals, readSignalByRuleId, findSignalInArrayByRuleId } from './read_signals'; +import { readRules, readRuleByRuleId, findRuleInArrayByRuleId } from './read_rules'; import { AlertsClient } from '../../../../../alerting'; import { getResult, @@ -14,19 +14,19 @@ import { } from '../routes/__mocks__/request_responses'; import { SIGNALS_ID } from '../../../../common/constants'; -describe('read_signals', () => { - describe('readSignals', () => { +describe('read_rules', () => { + describe('readRules', () => { test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is set but ruleId is null', async () => { @@ -34,12 +34,12 @@ describe('read_signals', () => { alertsClient.get.mockResolvedValue(getResult()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: null, }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { @@ -48,12 +48,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: undefined, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is null but ruleId is set', async () => { @@ -62,12 +62,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: null, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return null if id and ruleId are null', async () => { @@ -76,12 +76,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: null, ruleId: null, }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('should return null if id and ruleId are undefined', async () => { @@ -90,27 +90,27 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: undefined, ruleId: undefined, }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); - describe('readSignalByRuleId', () => { + describe('readRuleByRuleId', () => { test('should return a single value if the rule id matches', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should not return a single value if the rule id does not match', async () => { @@ -119,11 +119,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-that-should-not-match-anything', }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('should return a single value of rule-1 with multiple values', async () => { @@ -140,11 +140,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-1', }); - expect(signal).toEqual(result1); + expect(rule).toEqual(result1); }); test('should return a single value of rule-2 with multiple values', async () => { @@ -161,11 +161,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-2', }); - expect(signal).toEqual(result2); + expect(rule).toEqual(result2); }); test('should return null for a made up value with multiple values', async () => { @@ -182,57 +182,57 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-that-should-not-match-anything', }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); - describe('findSignalInArrayByRuleId', () => { + describe('findRuleInArrayByRuleId', () => { test('returns null if the objects are not of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: 'made up 1', params: { ruleId: '123' } }, { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('returns correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); }); test('returns second correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '456' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); }); test('returns null with correct types but data does not exist', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '892' ); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts similarity index 50% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts index 3c49112aaf50b..5c33526329016 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts @@ -4,20 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { findSignals } from './find_signals'; -import { SignalAlertType, isAlertTypeArray, ReadSignalParams, ReadSignalByRuleId } from './types'; +import { findRules } from './find_rules'; +import { RuleAlertType, isAlertTypeArray, ReadRuleParams, ReadRuleByRuleId } from './types'; -export const findSignalInArrayByRuleId = ( +export const findRuleInArrayByRuleId = ( objects: object[], ruleId: string -): SignalAlertType | null => { +): RuleAlertType | null => { if (isAlertTypeArray(objects)) { - const signals: SignalAlertType[] = objects; - const signal: SignalAlertType[] = signals.filter(datum => { + const rules: RuleAlertType[] = objects; + const rule: RuleAlertType[] = rules.filter(datum => { return datum.params.ruleId === ruleId; }); - if (signal.length !== 0) { - return signal[0]; + if (rule.length !== 0) { + return rule[0]; } else { return null; } @@ -26,32 +26,32 @@ export const findSignalInArrayByRuleId = ( } }; -// This an extremely slow and inefficient way of getting a signal by its id. -// I have to manually query every single record since the Signal Params are +// This an extremely slow and inefficient way of getting a rule by its id. +// I have to manually query every single record since the rule Params are // not indexed and I cannot push in my own _id when I create an alert at the moment. // TODO: Once we can directly push in the _id, then we should no longer need this way. // TODO: This is meant to be _very_ temporary. -export const readSignalByRuleId = async ({ +export const readRuleByRuleId = async ({ alertsClient, ruleId, -}: ReadSignalByRuleId): Promise => { - const firstSignals = await findSignals({ alertsClient, page: 1 }); - const firstSignal = findSignalInArrayByRuleId(firstSignals.data, ruleId); - if (firstSignal != null) { - return firstSignal; +}: ReadRuleByRuleId): Promise => { + const firstRules = await findRules({ alertsClient, page: 1 }); + const firstRule = findRuleInArrayByRuleId(firstRules.data, ruleId); + if (firstRule != null) { + return firstRule; } else { - const totalPages = Math.ceil(firstSignals.total / firstSignals.perPage); + const totalPages = Math.ceil(firstRules.total / firstRules.perPage); return Array(totalPages) .fill({}) .map((_, page) => { // page index never starts at zero. It always has to be 1 or greater - return findSignals({ alertsClient, page: page + 1 }); + return findRules({ alertsClient, page: page + 1 }); }) - .reduce>(async (accum, findSignal) => { - const signals = await findSignal; - const signal = findSignalInArrayByRuleId(signals.data, ruleId); - if (signal != null) { - return signal; + .reduce>(async (accum, findRule) => { + const rules = await findRule; + const rule = findRuleInArrayByRuleId(rules.data, ruleId); + if (rule != null) { + return rule; } else { return accum; } @@ -59,7 +59,7 @@ export const readSignalByRuleId = async ({ } }; -export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams) => { +export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => { if (id != null) { try { const output = await alertsClient.get({ id }); @@ -73,7 +73,7 @@ export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams } } } else if (ruleId != null) { - return readSignalByRuleId({ alertsClient, ruleId }); + return readRuleByRuleId({ alertsClient, ruleId }); } else { // should never get here, and yet here we are. return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts index 69eb3eb665060..91d7d18a4945c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts @@ -14,17 +14,17 @@ import { import { buildEventsSearchQuery } from './build_events_query'; import { searchAfterAndBulkCreate } from './utils'; -import { SignalAlertTypeDefinition } from './types'; +import { RuleAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; import { getInputOutputIndex } from './get_input_output_index'; -export const signalsAlertType = ({ +export const rulesAlertType = ({ logger, version, }: { logger: Logger; version: string; -}): SignalAlertTypeDefinition => { +}): RuleAlertTypeDefinition => { return { id: SIGNALS_ID, name: 'SIEM Signals', @@ -127,7 +127,7 @@ export const signalsAlertType = ({ const bulkIndexResult = await searchAfterAndBulkCreate({ someResult: noReIndexResult, - signalParams: params, + ruleParams: params, services, logger, id: alertId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 29eb7872f163d..28431b8165266 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -21,7 +21,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/server'; export type PartialFilter = Partial; -export interface SignalAlertParams { +export interface RuleAlertParams { description: string; enabled: boolean; falsePositives: string[]; @@ -47,31 +47,31 @@ export interface SignalAlertParams { type: 'filter' | 'query' | 'saved_query'; } -export type SignalAlertParamsRest = Omit< - SignalAlertParams, +export type RuleAlertParamsRest = Omit< + RuleAlertParams, 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' | 'riskScore' | 'outputIndex' > & { - rule_id: SignalAlertParams['ruleId']; - false_positives: SignalAlertParams['falsePositives']; - saved_id: SignalAlertParams['savedId']; - max_signals: SignalAlertParams['maxSignals']; - risk_score: SignalAlertParams['riskScore']; - output_index: SignalAlertParams['outputIndex']; + rule_id: RuleAlertParams['ruleId']; + false_positives: RuleAlertParams['falsePositives']; + saved_id: RuleAlertParams['savedId']; + max_signals: RuleAlertParams['maxSignals']; + risk_score: RuleAlertParams['riskScore']; + output_index: RuleAlertParams['outputIndex']; }; -export type OutputSignalAlertRest = SignalAlertParamsRest & { +export type OutputRuleAlertRest = RuleAlertParamsRest & { id: string; created_by: string | undefined | null; updated_by: string | undefined | null; }; -export type OutputSignalES = OutputSignalAlertRest & { +export type OutputRuleES = OutputRuleAlertRest & { status: 'open' | 'closed'; }; -export type UpdateSignalAlertParamsRest = Partial & { +export type UpdateRuleAlertParamsRest = Partial & { id: string | undefined; - rule_id: SignalAlertParams['ruleId'] | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; }; export interface FindParamsRest { @@ -88,18 +88,18 @@ export interface Clients { actionsClient: ActionsClient; } -export type SignalParams = SignalAlertParams & Clients; +export type RuleParams = RuleAlertParams & Clients; -export type UpdateSignalParams = Partial & { +export type UpdateRuleParams = Partial & { id: string | undefined | null; } & Clients; -export type DeleteSignalParams = Clients & { +export type DeleteRuleParams = Clients & { id: string | undefined; ruleId: string | undefined | null; }; -export interface FindSignalsRequest extends Omit { +export interface FindRulesRequest extends Omit { query: { per_page: number; page: number; @@ -111,7 +111,7 @@ export interface FindSignalsRequest extends Omit { }; } -export interface FindSignalParams { +export interface FindRuleParams { alertsClient: AlertsClient; perPage?: number; page?: number; @@ -121,34 +121,34 @@ export interface FindSignalParams { sortOrder?: 'asc' | 'desc'; } -export interface ReadSignalParams { +export interface ReadRuleParams { alertsClient: AlertsClient; id?: string | undefined | null; ruleId?: string | undefined | null; } -export interface ReadSignalByRuleId { +export interface ReadRuleByRuleId { alertsClient: AlertsClient; ruleId: string; } -export type AlertTypeParams = Omit; +export type RuleTypeParams = Omit; -export type SignalAlertType = Alert & { +export type RuleAlertType = Alert & { id: string; - params: AlertTypeParams; + params: RuleTypeParams; }; -export interface SignalsRequest extends RequestFacade { - payload: SignalAlertParamsRest; +export interface RulesRequest extends RequestFacade { + payload: RuleAlertParamsRest; } -export interface UpdateSignalsRequest extends RequestFacade { - payload: UpdateSignalAlertParamsRest; +export interface UpdateRulesRequest extends RequestFacade { + payload: UpdateRuleAlertParamsRest; } -export type SignalExecutorOptions = Omit & { - params: SignalAlertParams & { +export type RuleExecutorOptions = Omit & { + params: RuleAlertParams & { scrollSize: number; scrollLock: string; }; @@ -221,24 +221,24 @@ export type QueryRequest = Omit & { query: { id: string | undefined; rule_id: string | undefined }; }; -// This returns true because by default a SignalAlertTypeDefinition is an AlertType +// This returns true because by default a RuleAlertTypeDefinition is an AlertType // since we are only increasing the strictness of params. -export const isAlertExecutor = (obj: SignalAlertTypeDefinition): obj is AlertType => { +export const isAlertExecutor = (obj: RuleAlertTypeDefinition): obj is AlertType => { return true; }; -export type SignalAlertTypeDefinition = Omit & { - executor: ({ services, params, state }: SignalExecutorOptions) => Promise; +export type RuleAlertTypeDefinition = Omit & { + executor: ({ services, params, state }: RuleExecutorOptions) => Promise; }; -export const isAlertTypes = (obj: unknown[]): obj is SignalAlertType[] => { - return obj.every(signal => isAlertType(signal)); +export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { + return obj.every(rule => isAlertType(rule)); }; -export const isAlertType = (obj: unknown): obj is SignalAlertType => { +export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; -export const isAlertTypeArray = (objArray: unknown[]): objArray is SignalAlertType[] => { +export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { return objArray.length === 0 || isAlertType(objArray[0]); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts index 39f7951a8eab9..1022fea93200f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateInterval, calculateName } from './update_signals'; +import { calculateInterval, calculateName } from './update_rules'; -describe('update_signals', () => { +describe('update_rules', () => { describe('#calculateInterval', () => { - test('given a undefined interval, it returns the signalInterval ', () => { + test('given a undefined interval, it returns the ruleInterval ', () => { const interval = calculateInterval(undefined, '10m'); expect(interval).toEqual('10m'); }); - test('given a undefined signalInterval, it returns a undefined interval ', () => { + test('given a undefined ruleInterval, it returns a undefined interval ', () => { const interval = calculateInterval('10m', undefined); expect(interval).toEqual('10m'); }); - test('given both an undefined signalInterval and a undefined interval, it returns 5m', () => { + test('given both an undefined ruleInterval and a undefined interval, it returns 5m', () => { const interval = calculateInterval(undefined, undefined); expect(interval).toEqual('5m'); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts similarity index 66% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts index a38fd7756afa1..81360d7824230 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts @@ -6,17 +6,17 @@ import { defaults } from 'lodash/fp'; import { AlertAction } from '../../../../../alerting/server/types'; -import { readSignals } from './read_signals'; -import { UpdateSignalParams } from './types'; +import { readRules } from './read_rules'; +import { UpdateRuleParams } from './types'; export const calculateInterval = ( interval: string | undefined, - signalInterval: string | undefined + ruleInterval: string | undefined ): string => { if (interval != null) { return interval; - } else if (signalInterval != null) { - return signalInterval; + } else if (ruleInterval != null) { + return ruleInterval; } else { return '5m'; } @@ -35,13 +35,13 @@ export const calculateName = ({ return originalName; } else { // You really should never get to this point. This is a fail safe way to send back - // the name of "untitled" just in case a signal rule name became null or undefined at + // the name of "untitled" just in case a rule name became null or undefined at // some point since TypeScript allows it. return 'untitled'; } }; -export const updateSignal = async ({ +export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types description, @@ -68,17 +68,17 @@ export const updateSignal = async ({ to, type, references, -}: UpdateSignalParams) => { - const signal = await readSignals({ alertsClient, ruleId, id }); - if (signal == null) { +}: UpdateRuleParams) => { + const rule = await readRules({ alertsClient, ruleId, id }); + if (rule == null) { return null; } - // TODO: Remove this as cast as soon as signal.actions TypeScript bug is fixed + // TODO: Remove this as cast as soon as rule.actions TypeScript bug is fixed // where it is trying to return AlertAction[] or RawAlertAction[] - const actions = (signal.actions as AlertAction[] | undefined) || []; + const actions = (rule.actions as AlertAction[] | undefined) || []; - const params = signal.params || {}; + const params = rule.params || {}; const nextParams = defaults( { @@ -107,18 +107,18 @@ export const updateSignal = async ({ } ); - if (signal.enabled && !enabled) { - await alertsClient.disable({ id: signal.id }); - } else if (!signal.enabled && enabled) { - await alertsClient.enable({ id: signal.id }); + if (rule.enabled && !enabled) { + await alertsClient.disable({ id: rule.id }); + } else if (!rule.enabled && enabled) { + await alertsClient.enable({ id: rule.id }); } return alertsClient.update({ - id: signal.id, + id: rule.id, data: { tags: [], - name: calculateName({ updatedName: name, originalName: signal.name }), - interval: calculateInterval(interval, signal.interval), + name: calculateName({ updatedName: name, originalName: rule.name }), + interval: calculateInterval(interval, rule.interval), actions, params: nextParams, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index 4aac425c7f80f..19c8d5ccc87ca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -17,7 +17,7 @@ import { } from './utils'; import { sampleDocNoSortId, - sampleSignalAlertParams, + sampleRuleAlertParams, sampleDocSearchResultsNoSortId, sampleDocSearchResultsNoSortIdNoHits, sampleDocSearchResultsNoSortIdNoVersion, @@ -25,7 +25,7 @@ import { sampleEmptyDocSearchResults, repeatedSearchResultsWithSortId, sampleBulkCreateDuplicateResult, - sampleSignalId, + sampleRuleGuid, } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; @@ -52,11 +52,11 @@ describe('utils', () => { describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const fakeSignalSourceHit = buildBulkBody({ doc: sampleDocNoSortId(fakeUuid), - signalParams: sampleParams, - id: sampleSignalId, + ruleParams: sampleParams, + id: sampleRuleGuid, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', @@ -214,7 +214,7 @@ describe('utils', () => { }); test('create successful bulk create', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -227,10 +227,10 @@ describe('utils', () => { }); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -242,7 +242,7 @@ describe('utils', () => { }); test('create successful bulk create with docs with no versioning', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -255,10 +255,10 @@ describe('utils', () => { }); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -269,15 +269,15 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create unsuccessful bulk create due to empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -289,15 +289,15 @@ describe('utils', () => { }); test('create successful bulk create when bulk create has errors', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -312,12 +312,12 @@ describe('utils', () => { describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -326,11 +326,11 @@ describe('utils', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); const searchAfterResult = await singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -339,14 +339,14 @@ describe('utils', () => { }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); await expect( singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -356,13 +356,13 @@ describe('utils', () => { }); describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const result = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -375,7 +375,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs', async () => { - const sampleParams = sampleSignalAlertParams(30); + const sampleParams = sampleRuleAlertParams(30); const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -409,10 +409,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -426,14 +426,14 @@ describe('utils', () => { }); test('if unsuccessful first bulk create', async () => { const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -446,7 +446,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -459,10 +459,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortId(someUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -475,7 +475,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -488,10 +488,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortIdNoHits(someUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -503,7 +503,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const oneGuid = uuid.v4(); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster @@ -519,10 +519,10 @@ describe('utils', () => { .mockReturnValueOnce(sampleDocSearchResultsNoSortId(oneGuid)); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -534,7 +534,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -549,10 +549,10 @@ describe('utils', () => { .mockReturnValueOnce(sampleEmptyDocSearchResults); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -564,7 +564,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if returns false when singleSearchAfter throws an exception', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -581,10 +581,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index f2a3424655945..ba3f310c886ce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -13,13 +13,13 @@ import { SignalSourceHit, SignalSearchResponse, BulkResponse, - AlertTypeParams, - OutputSignalES, + RuleTypeParams, + OutputRuleES, } from './types'; import { buildEventsSearchQuery } from './build_events_query'; interface BuildRuleParams { - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; name: string; id: string; enabled: boolean; @@ -29,46 +29,46 @@ interface BuildRuleParams { } export const buildRule = ({ - signalParams, + ruleParams, name, id, enabled, createdBy, updatedBy, interval, -}: BuildRuleParams): Partial => { - return pickBy((value: unknown) => value != null, { +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { id, status: 'open', - rule_id: signalParams.ruleId, - false_positives: signalParams.falsePositives, - saved_id: signalParams.savedId, - meta: signalParams.meta, - max_signals: signalParams.maxSignals, - risk_score: signalParams.riskScore, - output_index: signalParams.outputIndex, - description: signalParams.description, - filter: signalParams.filter, - from: signalParams.from, - immutable: signalParams.immutable, - index: signalParams.index, + rule_id: ruleParams.ruleId, + false_positives: ruleParams.falsePositives, + saved_id: ruleParams.savedId, + meta: ruleParams.meta, + max_signals: ruleParams.maxSignals, + risk_score: ruleParams.riskScore, + output_index: ruleParams.outputIndex, + description: ruleParams.description, + filter: ruleParams.filter, + from: ruleParams.from, + immutable: ruleParams.immutable, + index: ruleParams.index, interval, - language: signalParams.language, + language: ruleParams.language, name, - query: signalParams.query, - references: signalParams.references, - severity: signalParams.severity, - tags: signalParams.tags, - type: signalParams.type, - to: signalParams.to, + query: ruleParams.query, + references: ruleParams.references, + severity: ruleParams.severity, + tags: ruleParams.tags, + type: ruleParams.type, + to: ruleParams.to, enabled, - filters: signalParams.filters, + filters: ruleParams.filters, created_by: createdBy, updated_by: updatedBy, }); }; -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { return { parent: { id: doc._id, @@ -83,7 +83,7 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial) interface BuildBulkBodyParams { doc: SignalSourceHit; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; id: string; name: string; createdBy: string; @@ -95,7 +95,7 @@ interface BuildBulkBodyParams { // format search_after result for signals index. export const buildBulkBody = ({ doc, - signalParams, + ruleParams, id, name, createdBy, @@ -104,7 +104,7 @@ export const buildBulkBody = ({ enabled, }: BuildBulkBodyParams): SignalHit => { const rule = buildRule({ - signalParams, + ruleParams, id, name, enabled, @@ -123,7 +123,7 @@ export const buildBulkBody = ({ interface SingleBulkCreateParams { someResult: SignalSearchResponse; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; id: string; @@ -148,7 +148,7 @@ export const generateId = ( // Bulk Index documents. export const singleBulkCreate = async ({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -179,11 +179,11 @@ export const singleBulkCreate = async ({ doc._index, doc._id, doc._version ? doc._version.toString() : '', - signalParams.ruleId ?? '' + ruleParams.ruleId ?? '' ), }, }, - buildBulkBody({ doc, signalParams, id, name, createdBy, updatedBy, interval, enabled }), + buildBulkBody({ doc, ruleParams, id, name, createdBy, updatedBy, interval, enabled }), ]); const time1 = performance.now(); const firstResult: BulkResponse = await services.callCluster('bulk', { @@ -222,7 +222,7 @@ export const singleBulkCreate = async ({ interface SingleSearchAfterParams { searchAfterSortId: string | undefined; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; pageSize: number; @@ -231,7 +231,7 @@ interface SingleSearchAfterParams { // utilize search_after for paging results into bulk. export const singleSearchAfter = async ({ searchAfterSortId, - signalParams, + ruleParams, services, logger, pageSize, @@ -241,10 +241,10 @@ export const singleSearchAfter = async ({ } try { const searchAfterQuery = buildEventsSearchQuery({ - index: signalParams.index, - from: signalParams.from, - to: signalParams.to, - filter: signalParams.filter, + index: ruleParams.index, + from: ruleParams.from, + to: ruleParams.to, + filter: ruleParams.filter, size: pageSize, searchAfterSortId, }); @@ -261,7 +261,7 @@ export const singleSearchAfter = async ({ interface SearchAfterAndBulkCreateParams { someResult: SignalSearchResponse; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; id: string; @@ -277,7 +277,7 @@ interface SearchAfterAndBulkCreateParams { // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -296,7 +296,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug('[+] starting bulk insertion'); await singleBulkCreate({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -314,8 +314,7 @@ export const searchAfterAndBulkCreate = async ({ // If the total number of hits for the overall search result is greater than // maxSignals, default to requesting a total of maxSignals, otherwise use the // totalHits in the response from the searchAfter query. - const maxTotalHitsSize = - totalHits >= signalParams.maxSignals ? signalParams.maxSignals : totalHits; + const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -336,7 +335,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug(`sortIds: ${sortIds}`); const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ searchAfterSortId: sortId, - signalParams, + ruleParams, services, logger, pageSize, // maximum number of docs to receive per search result. @@ -355,7 +354,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug('next bulk index'); await singleBulkCreate({ someResult: searchAfterResult, - signalParams, + ruleParams, services, logger, id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index c74d2e87a7ef6..4c49326fbb32a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,13 +6,13 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { SignalAlertParamsRest, SignalAlertType } from '../../alerts/types'; +import { RuleAlertParamsRest, RuleAlertType } from '../../alerts/types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; // The Omit of filter is because of a Hapi Server Typing issue that I am unclear // where it comes from. I would hope to remove the "filter" as an omit at some point // when we upgrade and Hapi Server is ok with the filter. -export const typicalPayload = (): Partial> => ({ +export const typicalPayload = (): Partial> => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -28,7 +28,7 @@ export const typicalPayload = (): Partial> language: 'kuery', }); -export const typicalFilterPayload = (): Partial => ({ +export const typicalFilterPayload = (): Partial => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -64,7 +64,7 @@ interface FindHit { page: number; perPage: number; total: number; - data: SignalAlertType[]; + data: RuleAlertType[]; } export const getFindResult = (): FindHit => ({ @@ -81,7 +81,7 @@ export const getFindResultWithSingleHit = (): FindHit => ({ data: [getResult()], }); -export const getFindResultWithMultiHits = (data: SignalAlertType[]): FindHit => ({ +export const getFindResultWithMultiHits = (data: RuleAlertType[]): FindHit => ({ page: 1, perPage: 1, total: 2, @@ -113,7 +113,7 @@ export const createActionResult = (): ActionResult => ({ config: {}, }); -export const getResult = (): SignalAlertType => ({ +export const getResult = (): RuleAlertType => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', tags: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts index 1232fe3ce219d..4c222c196300c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts @@ -10,7 +10,7 @@ import { createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { createSignalsRoute } from './create_signals_route'; +import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -21,17 +21,17 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('create_signals', () => { +describe('create_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - createSignalsRoute(server); + createRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when creating a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); @@ -42,14 +42,14 @@ describe('create_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - createSignalsRoute(serverWithoutActionClient); + createRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createSignalsRoute(serverWithoutAlertClient); + createRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); @@ -58,7 +58,7 @@ describe('create_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - createSignalsRoute(serverWithoutActionOrAlertClient); + createRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts similarity index 72% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts index fa8fd66ef2aef..7e1ac07e1f0aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -9,14 +9,14 @@ import { isFunction } from 'lodash/fp'; import Boom from 'boom'; import uuid from 'uuid'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { createSignals } from '../alerts/create_signals'; -import { SignalsRequest } from '../alerts/types'; -import { createSignalsSchema } from './schemas'; +import { createRules } from '../alerts/create_rules'; +import { RulesRequest } from '../alerts/types'; +import { createRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; -import { readSignals } from '../alerts/read_signals'; +import { readRules } from '../alerts/read_rules'; import { transformOrError } from './utils'; -export const createCreateSignalsRoute: Hapi.ServerRoute = { +export const createCreateRulesRoute: Hapi.ServerRoute = { method: 'POST', path: DETECTION_ENGINE_RULES_URL, options: { @@ -25,10 +25,10 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: createSignalsSchema, + payload: createRulesSchema, }, }, - async handler(request: SignalsRequest, headers) { + async handler(request: RulesRequest, headers) { const { description, enabled, @@ -63,13 +63,13 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { } if (ruleId != null) { - const signal = await readSignals({ alertsClient, ruleId }); - if (signal != null) { - return new Boom(`Signal rule_id ${ruleId} already exists`, { statusCode: 409 }); + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); } } - const createdSignal = await createSignals({ + const createdRule = await createRules({ alertsClient, actionsClient, description, @@ -96,10 +96,10 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { type, references, }); - return transformOrError(createdSignal); + return transformOrError(createdRule); }, }; -export const createSignalsRoute = (server: ServerFacade) => { - server.route(createCreateSignalsRoute); +export const createRulesRoute = (server: ServerFacade) => { + server.route(createCreateRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts index 95816aa55d1fe..0808051964dc1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { deleteSignalsRoute } from './delete_signals_route'; +import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -22,12 +22,12 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('delete_signals', () => { +describe('delete_rules', () => { let { server, alertsClient } = createMockServer(); beforeEach(() => { ({ server, alertsClient } = createMockServer()); - deleteSignalsRoute(server); + deleteRulesRoute(server); }); afterEach(() => { @@ -35,7 +35,7 @@ describe('delete_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by alertId', async () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -43,7 +43,7 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by id', async () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -51,7 +51,7 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - test('returns 404 when deleting a single signal that does not exist with a valid actionClient and alertClient', async () => { + test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -61,14 +61,14 @@ describe('delete_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - deleteSignalsRoute(serverWithoutActionClient); + deleteRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteSignalsRoute(serverWithoutAlertClient); + deleteRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); @@ -77,7 +77,7 @@ describe('delete_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - deleteSignalsRoute(serverWithoutActionOrAlertClient); + deleteRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts index 1f5494a54ddca..12dff0dd60c14 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts @@ -8,13 +8,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { deleteSignals } from '../alerts/delete_signals'; +import { deleteRules } from '../alerts/delete_rules'; import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; +import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; import { getIdError, transformOrError } from './utils'; -export const createDeleteSignalsRoute: Hapi.ServerRoute = { +export const createDeleteRulesRoute: Hapi.ServerRoute = { method: 'DELETE', path: DETECTION_ENGINE_RULES_URL, options: { @@ -23,7 +23,7 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: querySignalSchema, + query: queryRulesSchema, }, }, async handler(request: QueryRequest, headers) { @@ -35,21 +35,21 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signal = await deleteSignals({ + const rule = await deleteRules({ actionsClient, alertsClient, id, ruleId, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const deleteSignalsRoute = (server: ServerFacade): void => { - server.route(createDeleteSignalsRoute); +export const deleteRulesRoute = (server: ServerFacade): void => { + server.route(createDeleteRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts index be3dce36e8716..dae40f05155dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts @@ -11,17 +11,17 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { findSignalsRoute } from './find_signals_route'; +import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, getResult, getFindRequest } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('find_signals', () => { +describe('find_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { ({ server, alertsClient, actionsClient } = createMockServer()); - findSignalsRoute(server); + findRulesRoute(server); }); afterEach(() => { @@ -29,7 +29,7 @@ describe('find_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); actionsClient.find.mockResolvedValue({ page: 1, @@ -44,14 +44,14 @@ describe('find_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - findSignalsRoute(serverWithoutActionClient); + findRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findSignalsRoute(serverWithoutAlertClient); + findRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); @@ -60,7 +60,7 @@ describe('find_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - findSignalsRoute(serverWithoutActionOrAlertClient); + findRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts similarity index 70% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts index 120b71fab7d3a..893fb3f689d16 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts @@ -7,13 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { findSignals } from '../alerts/find_signals'; -import { FindSignalsRequest } from '../alerts/types'; -import { findSignalsSchema } from './schemas'; +import { findRules } from '../alerts/find_rules'; +import { FindRulesRequest } from '../alerts/types'; +import { findRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { transformFindAlertsOrError } from './utils'; -export const createFindSignalRoute: Hapi.ServerRoute = { +export const createFindRulesRoute: Hapi.ServerRoute = { method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find`, options: { @@ -22,10 +22,10 @@ export const createFindSignalRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: findSignalsSchema, + query: findRulesSchema, }, }, - async handler(request: FindSignalsRequest, headers) { + async handler(request: FindRulesRequest, headers) { const { query } = request; const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; @@ -34,7 +34,7 @@ export const createFindSignalRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signals = await findSignals({ + const rules = await findRules({ alertsClient, perPage: query.per_page, page: query.page, @@ -42,10 +42,10 @@ export const createFindSignalRoute: Hapi.ServerRoute = { sortOrder: query.sort_order, filter: query.filter, }); - return transformFindAlertsOrError(signals); + return transformFindAlertsOrError(rules); }, }; -export const findSignalsRoute = (server: ServerFacade) => { - server.route(createFindSignalRoute); +export const findRulesRoute = (server: ServerFacade) => { + server.route(createFindRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts index 021bcc7b8b48e..47ecf62f41be9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { readSignalsRoute } from './read_signals_route'; +import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -26,7 +26,7 @@ describe('read_signals', () => { beforeEach(() => { ({ server, alertsClient } = createMockServer()); - readSignalsRoute(server); + readRulesRoute(server); }); afterEach(() => { @@ -34,7 +34,7 @@ describe('read_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when reading a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadRequest()); @@ -43,14 +43,14 @@ describe('read_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - readSignalsRoute(serverWithoutActionClient); + readRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readSignalsRoute(serverWithoutAlertClient); + readRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); @@ -59,7 +59,7 @@ describe('read_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - readSignalsRoute(serverWithoutActionOrAlertClient); + readRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts index 2d662f9049cce..4642c34fbe339 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts @@ -9,12 +9,12 @@ import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; import { getIdError, transformOrError } from './utils'; -import { readSignals } from '../alerts/read_signals'; +import { readRules } from '../alerts/read_rules'; import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; +import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; -export const createReadSignalsRoute: Hapi.ServerRoute = { +export const createReadRulesRoute: Hapi.ServerRoute = { method: 'GET', path: DETECTION_ENGINE_RULES_URL, options: { @@ -23,7 +23,7 @@ export const createReadSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: querySignalSchema, + query: queryRulesSchema, }, }, async handler(request: QueryRequest, headers) { @@ -34,19 +34,19 @@ export const createReadSignalsRoute: Hapi.ServerRoute = { if (!alertsClient || !actionsClient) { return headers.response().code(404); } - const signal = await readSignals({ + const rule = await readRules({ alertsClient, id, ruleId, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const readSignalsRoute = (server: ServerFacade) => { - server.route(createReadSignalsRoute); +export const readRulesRoute = (server: ServerFacade) => { + server.route(createReadRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 6639dc6a3dfd6..6c7e5c4054326 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createSignalsSchema, - updateSignalSchema, - findSignalsSchema, - querySignalSchema, -} from './schemas'; -import { - SignalAlertParamsRest, - FindParamsRest, - UpdateSignalAlertParamsRest, -} from '../alerts/types'; +import { createRulesSchema, updateRulesSchema, findRulesSchema, queryRulesSchema } from './schemas'; +import { RuleAlertParamsRest, FindParamsRest, UpdateRuleAlertParamsRest } from '../alerts/types'; describe('schemas', () => { - describe('create signals schema', () => { + describe('create rules schema', () => { test('empty objects do not validate', () => { - expect(createSignalsSchema.validate>({}).error).toBeTruthy(); + expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -32,7 +23,7 @@ describe('schemas', () => { test('[rule_id] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -40,7 +31,7 @@ describe('schemas', () => { test('[rule_id, description] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -49,7 +40,7 @@ describe('schemas', () => { test('[rule_id, description, from] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -59,7 +50,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -70,7 +61,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -82,7 +73,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -95,7 +86,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -109,7 +100,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -124,7 +115,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -140,7 +131,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -158,7 +149,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -176,7 +167,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -195,7 +186,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -215,7 +206,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -233,7 +224,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -252,7 +243,7 @@ describe('schemas', () => { test('If filter type is set then filter is required', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -270,7 +261,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -290,7 +281,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -310,7 +301,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -330,7 +321,7 @@ describe('schemas', () => { test('allows references to be sent as valid', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -351,7 +342,7 @@ describe('schemas', () => { test('defaults references to an array', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -371,8 +362,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { references: number[] } + createRulesSchema.validate< + Partial> & { references: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -394,8 +385,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { index: number[] } + createRulesSchema.validate< + Partial> & { index: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -416,7 +407,7 @@ describe('schemas', () => { test('defaults interval to 5 min', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -433,7 +424,7 @@ describe('schemas', () => { test('defaults max signals to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -451,7 +442,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -471,7 +462,7 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -489,7 +480,7 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -508,7 +499,7 @@ describe('schemas', () => { test('saved_query type can have filters with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -528,8 +519,8 @@ describe('schemas', () => { test('filters cannot be a string', () => { expect( - createSignalsSchema.validate< - Partial & { filters: string }> + createRulesSchema.validate< + Partial & { filters: string }> >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -550,7 +541,7 @@ describe('schemas', () => { test('saved_query type cannot have filter with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -570,7 +561,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -591,7 +582,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -612,7 +603,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -633,7 +624,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -655,7 +646,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -677,7 +668,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -699,7 +690,7 @@ describe('schemas', () => { test('You can optionally send in an array of tags', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -722,32 +713,32 @@ describe('schemas', () => { test('You cannot send in an array of tags that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { tags: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - }).error + createRulesSchema.validate> & { tags: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: [0, 1, 2], + } + ).error ).toBeTruthy(); }); test('You can optionally send in an array of false positives', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -770,8 +761,8 @@ describe('schemas', () => { test('You cannot send in an array of false positives that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { false_positives: number[] } + createRulesSchema.validate< + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -795,7 +786,7 @@ describe('schemas', () => { test('You can optionally set the immutable to be true', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -818,8 +809,8 @@ describe('schemas', () => { test('You cannot set the immutable to be a number', () => { expect( - createSignalsSchema.validate< - Partial> & { immutable: number } + createRulesSchema.validate< + Partial> & { immutable: number } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -843,7 +834,7 @@ describe('schemas', () => { test('You cannot set the risk_score to 101', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 101, @@ -866,7 +857,7 @@ describe('schemas', () => { test('You cannot set the risk_score to -1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: -1, @@ -889,7 +880,7 @@ describe('schemas', () => { test('You can set the risk_score to 0', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 0, @@ -912,7 +903,7 @@ describe('schemas', () => { test('You can set the risk_score to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 100, @@ -935,7 +926,7 @@ describe('schemas', () => { test('You can set meta to any object you want', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -961,9 +952,7 @@ describe('schemas', () => { test('You cannot create meta as a string', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -987,9 +976,7 @@ describe('schemas', () => { test('You can have an empty query string when filters are present', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1013,9 +1000,7 @@ describe('schemas', () => { test('You can omit the query string when filters are present', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1038,9 +1023,7 @@ describe('schemas', () => { test('query string defaults to empty string when present with filters', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1062,16 +1045,14 @@ describe('schemas', () => { }); }); - describe('update signals schema', () => { + describe('update rules schema', () => { test('empty objects do not validate as they require at least id or rule_id', () => { - expect( - updateSignalSchema.validate>({}).error - ).toBeTruthy(); + expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1079,7 +1060,7 @@ describe('schemas', () => { test('[id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', }).error ).toBeFalsy(); @@ -1087,7 +1068,7 @@ describe('schemas', () => { test('[rule_id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeFalsy(); @@ -1095,7 +1076,7 @@ describe('schemas', () => { test('[id and rule_id] does not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'id-1', rule_id: 'rule-1', }).error @@ -1104,7 +1085,7 @@ describe('schemas', () => { test('[rule_id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -1113,7 +1094,7 @@ describe('schemas', () => { test('[id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', }).error @@ -1122,7 +1103,7 @@ describe('schemas', () => { test('[id, risk_score] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', risk_score: 10, }).error @@ -1131,7 +1112,7 @@ describe('schemas', () => { test('[rule_id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1141,7 +1122,7 @@ describe('schemas', () => { test('[id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1151,7 +1132,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1162,7 +1143,7 @@ describe('schemas', () => { test('[id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1173,7 +1154,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1185,7 +1166,7 @@ describe('schemas', () => { test('[id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1197,7 +1178,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1210,7 +1191,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1223,7 +1204,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1237,7 +1218,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1251,7 +1232,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1266,7 +1247,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1281,7 +1262,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1297,7 +1278,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1313,7 +1294,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1330,7 +1311,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1347,7 +1328,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1365,7 +1346,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1383,7 +1364,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1400,7 +1381,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1417,7 +1398,7 @@ describe('schemas', () => { test('If filter type is set then filter is still not required', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1433,7 +1414,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1451,7 +1432,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1469,7 +1450,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1487,7 +1468,7 @@ describe('schemas', () => { test('allows references to be sent as a valid value to update with', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1506,7 +1487,7 @@ describe('schemas', () => { test('does not default references to an array', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1524,7 +1505,7 @@ describe('schemas', () => { test('does not default interval', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1539,7 +1520,7 @@ describe('schemas', () => { test('does not default max signal', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1555,8 +1536,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { references: number[] } + updateRulesSchema.validate< + Partial> & { references: number[] } >({ id: 'rule-1', description: 'some description', @@ -1576,8 +1557,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { index: number[] } + updateRulesSchema.validate< + Partial> & { index: number[] } >({ id: 'rule-1', description: 'some description', @@ -1596,7 +1577,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1614,7 +1595,7 @@ describe('schemas', () => { test('saved_id is not required when type is saved_query and will validate without it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1630,7 +1611,7 @@ describe('schemas', () => { test('saved_id validates with saved_query', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1647,7 +1628,7 @@ describe('schemas', () => { test('saved_query type can have filters with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1665,7 +1646,7 @@ describe('schemas', () => { test('saved_query type cannot have filter with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1683,7 +1664,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1702,7 +1683,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1721,7 +1702,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1740,7 +1721,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1760,7 +1741,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1780,7 +1761,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1800,7 +1781,7 @@ describe('schemas', () => { test('meta can be updated', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', meta: { whateverYouWant: 'anything_at_all' }, }).error @@ -1809,8 +1790,8 @@ describe('schemas', () => { test('You update meta as a string', () => { expect( - updateSignalSchema.validate< - Partial & { meta: string }> + updateRulesSchema.validate< + Partial & { meta: string }> >({ id: 'rule-1', meta: 'should not work', @@ -1820,8 +1801,8 @@ describe('schemas', () => { test('filters cannot be a string', () => { expect( - updateSignalSchema.validate< - Partial & { filters: string }> + updateRulesSchema.validate< + Partial & { filters: string }> >({ rule_id: 'rule-1', type: 'query', @@ -1831,14 +1812,14 @@ describe('schemas', () => { }); }); - describe('find signals schema', () => { + describe('find rules schema', () => { test('empty objects do validate', () => { - expect(findSignalsSchema.validate>({}).error).toBeFalsy(); + expect(findRulesSchema.validate>({}).error).toBeFalsy(); }); test('all values validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ per_page: 5, page: 1, sort_field: 'some field', @@ -1851,7 +1832,7 @@ describe('schemas', () => { test('made up parameters do not validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1859,31 +1840,31 @@ describe('schemas', () => { test('per_page validates', () => { expect( - findSignalsSchema.validate>({ per_page: 5 }).error + findRulesSchema.validate>({ per_page: 5 }).error ).toBeFalsy(); }); test('page validates', () => { expect( - findSignalsSchema.validate>({ page: 5 }).error + findRulesSchema.validate>({ page: 5 }).error ).toBeFalsy(); }); test('sort_field validates', () => { expect( - findSignalsSchema.validate>({ sort_field: 'some value' }).error + findRulesSchema.validate>({ sort_field: 'some value' }).error ).toBeFalsy(); }); test('fields validates with a string', () => { expect( - findSignalsSchema.validate>({ fields: ['some value'] }).error + findRulesSchema.validate>({ fields: ['some value'] }).error ).toBeFalsy(); }); test('fields validates with multiple strings', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ fields: ['some value 1', 'some value 2'], }).error ).toBeFalsy(); @@ -1891,23 +1872,23 @@ describe('schemas', () => { test('fields does not validate with a number', () => { expect( - findSignalsSchema.validate> & { fields: number[] }>({ + findRulesSchema.validate> & { fields: number[] }>({ fields: [5], }).error ).toBeTruthy(); }); test('per page has a default of 20', () => { - expect(findSignalsSchema.validate>({}).value.per_page).toEqual(20); + expect(findRulesSchema.validate>({}).value.per_page).toEqual(20); }); test('page has a default of 1', () => { - expect(findSignalsSchema.validate>({}).value.page).toEqual(1); + expect(findRulesSchema.validate>({}).value.page).toEqual(1); }); test('filter works with a string', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ filter: 'some value 1', }).error ).toBeFalsy(); @@ -1915,7 +1896,7 @@ describe('schemas', () => { test('filter does not work with a number', () => { expect( - findSignalsSchema.validate> & { filter: number }>({ + findRulesSchema.validate> & { filter: number }>({ filter: 5, }).error ).toBeTruthy(); @@ -1923,7 +1904,7 @@ describe('schemas', () => { test('sort_order requires sort_field to work', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'asc', }).error ).toBeTruthy(); @@ -1931,7 +1912,7 @@ describe('schemas', () => { test('sort_order and sort_field validate together', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'asc', sort_field: 'some field', }).error @@ -1940,7 +1921,7 @@ describe('schemas', () => { test('sort_order validates with desc and sort_field', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'desc', sort_field: 'some field', }).error @@ -1949,7 +1930,7 @@ describe('schemas', () => { test('sort_order does not validate with a string other than asc and desc', () => { expect( - findSignalsSchema.validate< + findRulesSchema.validate< Partial> & { sort_order: string } >({ sort_order: 'some other string', @@ -1959,29 +1940,27 @@ describe('schemas', () => { }); }); - describe('querySignalSchema', () => { + describe('queryRulesSchema', () => { test('empty objects do not validate', () => { - expect( - querySignalSchema.validate>({}).error - ).toBeTruthy(); + expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); test('both rule_id and id being supplied dot not validate', () => { expect( - querySignalSchema.validate>({ rule_id: '1', id: '1' }) + queryRulesSchema.validate>({ rule_id: '1', id: '1' }) .error ).toBeTruthy(); }); test('only id validates', () => { expect( - querySignalSchema.validate>({ id: '1' }).error + queryRulesSchema.validate>({ id: '1' }).error ).toBeFalsy(); }); test('only rule_id validates', () => { expect( - querySignalSchema.validate>({ rule_id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1' }).error ).toBeFalsy(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index 177e7cbebc213..664a98ad7d7dd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -52,7 +52,7 @@ const fields = Joi.array() .single(); /* eslint-enable @typescript-eslint/camelcase */ -export const createSignalsSchema = Joi.object({ +export const createRulesSchema = Joi.object({ description: description.required(), enabled: enabled.default(true), false_positives: false_positives.default([]), @@ -113,7 +113,7 @@ export const createSignalsSchema = Joi.object({ references: references.default([]), }); -export const updateSignalSchema = Joi.object({ +export const updateRulesSchema = Joi.object({ description, enabled, false_positives, @@ -168,12 +168,12 @@ export const updateSignalSchema = Joi.object({ references, }).xor('id', 'rule_id'); -export const querySignalSchema = Joi.object({ +export const queryRulesSchema = Joi.object({ rule_id, id, }).xor('id', 'rule_id'); -export const findSignalsSchema = Joi.object({ +export const findRulesSchema = Joi.object({ fields, filter: queryFilter, per_page, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts index 7288d18628316..d03d68417dd5d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { updateSignalsRoute } from './update_signals_route'; +import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -24,17 +24,17 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('update_signals', () => { +describe('update_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - updateSignalsRoute(server); + updateRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -43,7 +43,7 @@ describe('update_signals', () => { expect(statusCode).toBe(200); }); - test('returns 404 when updating a single signal that does not exist', async () => { + test('returns 404 when updating a single rule that does not exist', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -54,14 +54,14 @@ describe('update_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - updateSignalsRoute(serverWithoutActionClient); + updateRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateSignalsRoute(serverWithoutAlertClient); + updateRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); @@ -70,7 +70,7 @@ describe('update_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - updateSignalsRoute(serverWithoutActionOrAlertClient); + updateRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts similarity index 78% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts index 1dc54f34bd1f7..1cc65054527c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts @@ -7,13 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { updateSignal } from '../alerts/update_signals'; -import { UpdateSignalsRequest } from '../alerts/types'; -import { updateSignalSchema } from './schemas'; +import { updateRules } from '../alerts/update_rules'; +import { UpdateRulesRequest } from '../alerts/types'; +import { updateRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { getIdError, transformOrError } from './utils'; -export const createUpdateSignalsRoute: Hapi.ServerRoute = { +export const createUpdateRulesRoute: Hapi.ServerRoute = { method: 'PUT', path: DETECTION_ENGINE_RULES_URL, options: { @@ -22,10 +22,10 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: updateSignalSchema, + payload: updateRulesSchema, }, }, - async handler(request: UpdateSignalsRequest, headers) { + async handler(request: UpdateRulesRequest, headers) { const { description, enabled, @@ -60,7 +60,7 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signal = await updateSignal({ + const rule = await updateRules({ alertsClient, actionsClient, description, @@ -88,14 +88,14 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { type, references, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const updateSignalsRoute = (server: ServerFacade) => { - server.route(createUpdateSignalsRoute); +export const updateRulesRoute = (server: ServerFacade) => { + server.route(createUpdateRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index ed9e00735c704..632778d78dab7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { - transformAlertToSignal, + transformAlertToRule, getIdError, transformFindAlertsOrError, transformOrError, @@ -14,11 +14,11 @@ import { import { getResult } from './__mocks__/request_responses'; describe('utils', () => { - describe('transformAlertToSignal', () => { + describe('transformAlertToRule', () => { test('should work with a full data set', () => { - const fullSignal = getResult(); - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -45,8 +45,8 @@ describe('utils', () => { }); test('should work with a partial data set missing data', () => { - const fullSignal = getResult(); - const { from, language, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + const { from, language, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', @@ -72,10 +72,10 @@ describe('utils', () => { }); test('should omit query if query is null', () => { - const fullSignal = getResult(); - fullSignal.params.query = null; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = null; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -101,10 +101,10 @@ describe('utils', () => { }); test('should omit query if query is undefined', () => { - const fullSignal = getResult(); - fullSignal.params.query = undefined; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = undefined; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -130,10 +130,10 @@ describe('utils', () => { }); test('should omit a mix of undefined, null, and missing fields', () => { - const fullSignal = getResult(); - fullSignal.params.query = undefined; - fullSignal.params.language = null; - const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + fullRule.params.query = undefined; + fullRule.params.language = null; + const { from, enabled, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', @@ -157,10 +157,10 @@ describe('utils', () => { }); test('should return enabled is equal to false', () => { - const fullSignal = getResult(); - fullSignal.enabled = false; - const signalWithEnabledFalse = transformAlertToSignal(fullSignal); - expect(signalWithEnabledFalse).toEqual({ + const fullRule = getResult(); + fullRule.enabled = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: false, @@ -187,10 +187,10 @@ describe('utils', () => { }); test('should return immutable is equal to false', () => { - const fullSignal = getResult(); - fullSignal.params.immutable = false; - const signalWithEnabledFalse = transformAlertToSignal(fullSignal); - expect(signalWithEnabledFalse).toEqual({ + const fullRule = getResult(); + fullRule.params.immutable = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 7b9921b0375d8..eb0ae49436bca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { pickBy } from 'lodash/fp'; -import { SignalAlertType, isAlertType, OutputSignalAlertRest, isAlertTypes } from '../alerts/types'; +import { RuleAlertType, isAlertType, OutputRuleAlertRest, isAlertTypes } from '../alerts/types'; export const getIdError = ({ id, @@ -26,49 +26,49 @@ export const getIdError = ({ // Transforms the data but will remove any null or undefined it encounters and not include // those on the export -export const transformAlertToSignal = (signal: SignalAlertType): Partial => { - return pickBy((value: unknown) => value != null, { - created_by: signal.createdBy, - description: signal.params.description, - enabled: signal.enabled, - false_positives: signal.params.falsePositives, - filter: signal.params.filter, - filters: signal.params.filters, - from: signal.params.from, - id: signal.id, - immutable: signal.params.immutable, - index: signal.params.index, - interval: signal.interval, - rule_id: signal.params.ruleId, - language: signal.params.language, - output_index: signal.params.outputIndex, - max_signals: signal.params.maxSignals, - risk_score: signal.params.riskScore, - name: signal.name, - query: signal.params.query, - references: signal.params.references, - saved_id: signal.params.savedId, - meta: signal.params.meta, - severity: signal.params.severity, - updated_by: signal.updatedBy, - tags: signal.params.tags, - to: signal.params.to, - type: signal.params.type, +export const transformAlertToRule = (alert: RuleAlertType): Partial => { + return pickBy((value: unknown) => value != null, { + created_by: alert.createdBy, + description: alert.params.description, + enabled: alert.enabled, + false_positives: alert.params.falsePositives, + filter: alert.params.filter, + filters: alert.params.filters, + from: alert.params.from, + id: alert.id, + immutable: alert.params.immutable, + index: alert.params.index, + interval: alert.interval, + rule_id: alert.params.ruleId, + language: alert.params.language, + output_index: alert.params.outputIndex, + max_signals: alert.params.maxSignals, + risk_score: alert.params.riskScore, + name: alert.name, + query: alert.params.query, + references: alert.params.references, + saved_id: alert.params.savedId, + meta: alert.params.meta, + severity: alert.params.severity, + updated_by: alert.updatedBy, + tags: alert.params.tags, + to: alert.params.to, + type: alert.params.type, }); }; export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { if (isAlertTypes(findResults.data)) { - findResults.data = findResults.data.map(signal => transformAlertToSignal(signal)); + findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); return findResults; } else { return new Boom('Internal error transforming', { statusCode: 500 }); } }; -export const transformOrError = (signal: unknown): Partial | Boom => { - if (isAlertType(signal)) { - return transformAlertToSignal(signal); +export const transformOrError = (alert: unknown): Partial | Boom => { + if (isAlertType(alert)) { + return transformAlertToRule(alert); } else { return new Boom('Internal error transforming', { statusCode: 500 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md index b3ab0011e1f8f..8d617a8de3fcd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md @@ -4,6 +4,7 @@ search which is not available in the DEV console for the detection engine. Before beginning ensure in your .zshrc/.bashrc you have your user, password, and url set: Open up your .zshrc/.bashrc and add these lines with the variables filled in: + ``` export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} @@ -21,6 +22,7 @@ And that you have the latest version of [NodeJS](https://nodejs.org/en/), [CURL](https://curl.haxx.se), and [jq](https://stedolan.github.io/jq/) installed. If you have homebrew you can install using brew like so + ``` brew install jq ``` @@ -29,10 +31,9 @@ After that you can execute scripts within this folder by first ensuring your current working directory is `./scripts` and then running any scripts within that folder. -Example to add a signal to the system +Example to add a rule to the system ``` cd ./scripts -./post_signal.sh ./signals/root_or_admin_1.json +./post_rule.sh ./rules/root_or_admin_1.json ``` - diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh index 802273c67849d..e4d345eec0b65 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh @@ -9,4 +9,4 @@ set -e ./check_env_variables.sh -node ../../../../scripts/convert_saved_search_to_signals.js $1 $2 +node ../../../../scripts/convert_saved_search_to_rules.js $1 $2 diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh index 25cd4bfd33628..2db5740c79bb8 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal_by_id.sh ${id} +# Example: ./delete_rule_by_id.sh ${id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh index b74ee260ad8ad..80ef849828b78 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal_by_rule_id.sh ${rule_id} +# Example: ./delete_rule_by_rule_id.sh ${rule_id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh similarity index 81% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh index 34c3c401b4112..34b6208947c57 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh @@ -11,8 +11,8 @@ set -e FILTER=${1:-'alert.attributes.enabled:%20true'} -# Example: ./find_signal_by_filter.sh "alert.attributes.enabled:%20true" -# Example: ./find_signal_by_filter.sh "alert.attributes.name:%20Detect*" +# Example: ./find_rule_by_filter.sh "alert.attributes.enabled:%20true" +# Example: ./find_rule_by_filter.sh "alert.attributes.name:%20Detect*" # The %20 is just an encoded space that is typical of URL's. # Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp curl -s -k \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh index 4542eb7c9a827..520b4afa24cd2 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./find_signals.sh +# Example: ./find_rules.sh curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh index 122f18bbb80e5..8e6690d848db4 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh @@ -12,7 +12,7 @@ set -e SORT=${1:-'enabled'} ORDER=${2:-'asc'} -# Example: ./find_signals_sort.sh enabled asc +# Example: ./find_rules_sort.sh enabled asc curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?sort_field=$SORT&sort_order=$ORDER" \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh index 239a04846b11a..dba5652390ea9 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_by_id.sh {rule_id} +# Example: ./get_rule_by_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh index 5100caac32491..114b6570a03e2 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_by_rule_id.sh {rule_id} +# Example: ./get_rule_by_rule_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh similarity index 68% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh index b8bd0e0e0361f..591cf7625e2e3 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh @@ -10,20 +10,20 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_1.json}) +RULES=(${@:-./rules/root_or_admin_1.json}) -# Example: ./post_signal.sh -# Example: ./post_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" +# Example: ./post_rule.sh +# Example: ./post_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" do { - [ -e "$SIGNAL" ] || continue + [ -e "$RULE" ] || continue curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d @${SIGNAL} \ + -d @${RULE} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh index abb2111a91c1b..53e7bb504746d 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh @@ -12,8 +12,8 @@ set -e # Uses a default of 100 if no argument is specified NUMBER=${1:-100} -# Example: ./post_x_signals.sh -# Example: ./post_x_signals.sh 200 +# Example: ./post_x_rules.sh +# Example: ./post_x_rules.sh 200 for i in $(seq 1 $NUMBER); do { curl -s -k \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh similarity index 67% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh index 04541e1df1fa1..8e1abc7045602 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh @@ -10,20 +10,20 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_update_1.json}) +RULES=(${@:-./rules/root_or_admin_update_1.json}) -# Example: ./update_signal.sh -# Example: ./update_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" +# Example: ./update_rule.sh +# Example: ./update_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" do { - [ -e "$SIGNAL" ] || continue + [ -e "$RULE" ] || continue curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d @${SIGNAL} \ + -d @${RULE} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 13d040b969545..9c0059d0d109d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -23,7 +23,7 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { SignalAlertParamsRest } from './detection_engine/alerts/types'; +import { RuleAlertParamsRest } from './detection_engine/alerts/types'; export * from './hosts'; @@ -66,7 +66,7 @@ export interface SiemContext { } export interface Signal { - rule: Partial; + rule: Partial; parent: { id: string; type: string; From e721ec4ca8d5089d0778579893be0e3c77081f47 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 26 Nov 2019 09:17:06 -0600 Subject: [PATCH 089/128] [APM] Replace StaticIndexPattern with IIndexPattern (#51689) --- .../app/Main/__test__/UpdateBreadcrumbs.test.js | 1 - .../apm/public/components/shared/KueryBar/index.tsx | 11 ++++------- .../plugins/apm/server/lib/helpers/setup_request.ts | 3 +-- .../lib/index_pattern/get_dynamic_index_pattern.ts | 3 +-- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js index 8ddf48e79f911..41fb12be284ad 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js @@ -10,7 +10,6 @@ import { MemoryRouter } from 'react-router-dom'; import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); const coreMock = { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 24d320505c994..52be4d4fba774 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; @@ -19,7 +18,8 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; import { AutocompleteSuggestion, - AutocompleteProvider + AutocompleteProvider, + IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { usePlugins } from '../../../new-platform/plugin'; @@ -33,10 +33,7 @@ interface State { isLoadingSuggestions: boolean; } -function convertKueryToEsQuery( - kuery: string, - indexPattern: StaticIndexPattern -) { +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { const ast = fromKueryExpression(kuery); return toElasticsearchQuery(ast, indexPattern); } @@ -44,7 +41,7 @@ function convertKueryToEsQuery( function getSuggestions( query: string, selectionStart: number, - indexPattern: StaticIndexPattern, + indexPattern: IIndexPattern, boolFilter: unknown, autocompleteProvider?: AutocompleteProvider ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index 8f19f4baed7ee..a09cdbf91ec6e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -6,7 +6,6 @@ import moment from 'moment'; import { KibanaRequest } from 'src/core/server'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { IIndexPattern } from 'src/plugins/data/common'; import { APMConfig } from '../../../../../../plugins/apm/server'; import { @@ -22,7 +21,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { getDynamicIndexPattern } from '../index_pattern/get_dynamic_index_pattern'; function decodeUiFilters( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFiltersEncoded?: string ) { if (!uiFiltersEncoded || !indexPattern) { diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index f113e645ed95f..9eb99b7c21e75 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StaticIndexPattern } from 'ui/index_patterns'; import { APICaller } from 'src/core/server'; import LRU from 'lru-cache'; import { @@ -51,7 +50,7 @@ export const getDynamicIndexPattern = async ({ pattern: patternIndices }); - const indexPattern: StaticIndexPattern = { + const indexPattern: IIndexPattern = { fields, title: indexPatternTitle }; From 38c17d6c7d03c2697f92f95acceb828fd513bb0c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 26 Nov 2019 10:47:40 -0500 Subject: [PATCH 090/128] Improve session idle timeout, add session lifespan (#49855) This adds an absolute session timeout (lifespan) to user sessions. It also improves the existing session timeout toast and the overall user experience in several ways. --- docs/settings/security-settings.asciidoc | 12 +- .../security/authentication/index.asciidoc | 7 +- docs/user/security/securing-kibana.asciidoc | 27 +- .../resources/bin/kibana-docker | 2 + src/legacy/core_plugins/status_page/index.js | 5 + src/plugins/status_page/kibana.json | 6 + src/plugins/status_page/public/index.ts | 24 + src/plugins/status_page/public/plugin.ts | 39 ++ x-pack/legacy/plugins/security/index.js | 14 +- .../public/hacks/on_session_timeout.js | 14 +- .../public/views/logged_out/logged_out.tsx | 2 +- x-pack/package.json | 1 + x-pack/plugins/security/public/plugin.ts | 24 +- .../public/session/session_expired.test.ts | 97 ++-- .../public/session/session_expired.ts | 9 +- ... => session_idle_timeout_warning.test.tsx} | 8 +- .../session/session_idle_timeout_warning.tsx | 64 +++ .../session/session_lifespan_warning.tsx | 48 ++ .../public/session/session_timeout.mock.ts | 2 + .../public/session/session_timeout.test.tsx | 429 +++++++++++++----- .../public/session/session_timeout.tsx | 200 ++++++-- .../session_timeout_http_interceptor.ts | 4 +- .../session/session_timeout_warning.tsx | 39 -- ...thorized_response_http_interceptor.test.ts | 9 +- x-pack/plugins/security/public/types.ts | 12 + .../authentication/authenticator.test.ts | 340 ++++++++++++-- .../server/authentication/authenticator.ts | 82 +++- .../server/authentication/index.mock.ts | 1 + .../security/server/authentication/index.ts | 14 +- .../authentication/providers/token.test.ts | 10 +- .../server/authentication/providers/token.ts | 20 +- x-pack/plugins/security/server/config.test.ts | 97 ++-- x-pack/plugins/security/server/config.ts | 20 +- x-pack/plugins/security/server/plugin.test.ts | 11 +- x-pack/plugins/security/server/plugin.ts | 10 +- .../server/routes/authentication/index.ts | 2 + .../server/routes/authentication/session.ts | 46 ++ .../translations/translations/ja-JP.json | 5 +- .../translations/translations/zh-CN.json | 5 +- .../api_integration/apis/security/index.js | 1 + .../api_integration/apis/security/session.ts | 87 ++++ x-pack/test/api_integration/config.js | 1 + yarn.lock | 59 ++- 43 files changed, 1524 insertions(+), 385 deletions(-) create mode 100644 src/plugins/status_page/kibana.json create mode 100644 src/plugins/status_page/public/index.ts create mode 100644 src/plugins/status_page/public/plugin.ts rename x-pack/plugins/security/public/session/{session_timeout_warning.test.tsx => session_idle_timeout_warning.test.tsx} (71%) create mode 100644 x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx create mode 100644 x-pack/plugins/security/public/session/session_lifespan_warning.tsx delete mode 100644 x-pack/plugins/security/public/session/session_timeout_warning.tsx create mode 100644 x-pack/plugins/security/public/types.ts create mode 100644 x-pack/plugins/security/server/routes/authentication/session.ts create mode 100644 x-pack/test/api_integration/apis/security/session.ts diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 805d991a9a0f3..a2c05e4d87325 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -49,10 +49,16 @@ is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). -`xpack.security.sessionTimeout`:: +`xpack.security.session.idleTimeout`:: Sets the session duration (in milliseconds). By default, sessions stay active -until the browser is closed. When this is set to an explicit timeout, closing the -browser still requires the user to log back in to {kib}. +until the browser is closed. When this is set to an explicit idle timeout, closing +the browser still requires the user to log back in to {kib}. + +`xpack.security.session.lifespan`:: +Sets the maximum duration (in milliseconds), also known as "absolute timeout". By +default, a session can be renewed indefinitely. When this value is set, a session +will end once its lifespan is exceeded, even if the user is not idle. NOTE: if +`idleTimeout` is not set, this setting will still cause sessions to expire. `xpack.security.loginAssistanceMessage`:: Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index c2b1adc5e1b92..e6b70fa059fc2 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -188,9 +188,10 @@ The following sections apply both to <> and <> Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider -for every request that requires authentication. It also means that the {kib} session depends on the `xpack.security.sessionTimeout` -setting and the user is automatically logged out if the session expires. An access token that is stored in the session cookie -can expire, in which case {kib} will automatically renew it with a one-time-use refresh token and store it in the same cookie. +for every request that requires authentication. It also means that the {kib} session depends on the <> settings, and the user is automatically logged +out if the session expires. An access token that is stored in the session cookie can expire, in which case {kib} will +automatically renew it with a one-time-use refresh token and store it in the same cookie. {kib} can only determine if an access token has expired if it receives a request that requires authentication. If both access and refresh tokens have already expired (for example, after 24 hours of inactivity), {kib} initiates a new "handshake" and diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 1c74bd98642a7..2fbc6ba4f1ee6 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -56,16 +56,31 @@ xpack.security.encryptionKey: "something_at_least_32_characters" For more information, see <>. -- -. Optional: Change the default session duration. By default, sessions stay -active until the browser is closed. To change the duration, set the -`xpack.security.sessionTimeout` property in the `kibana.yml` configuration file. -The timeout is specified in milliseconds. For example, set the timeout to 600000 -to expire sessions after 10 minutes: +. Optional: Set a timeout to expire idle sessions. By default, a session stays +active until the browser is closed. To define a sliding session expiration, set +the `xpack.security.session.idleTimeout` property in the `kibana.yml` +configuration file. The idle timeout is specified in milliseconds. For example, +set the idle timeout to 600000 to expire idle sessions after 10 minutes: + -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.sessionTimeout: 600000 +xpack.security.session.idleTimeout: 600000 +-------------------------------------------------------------------------------- +-- + +. Optional: Change the maximum session duration or "lifespan" -- also known as +the "absolute timeout". By default, a session stays active until the browser is +closed. If an idle timeout is defined, a session can still be extended +indefinitely. To define a maximum session lifespan, set the +`xpack.security.session.lifespan` property in the `kibana.yml` configuration +file. The lifespan is specified in milliseconds. For example, set the lifespan +to 28800000 to expire sessions after 8 hours: ++ +-- +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.session.lifespan: 28800000 -------------------------------------------------------------------------------- -- diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 497307fa4124b..0c8faf47411d4 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -180,6 +180,8 @@ kibana_vars=( xpack.security.encryptionKey xpack.security.secureCookies xpack.security.sessionTimeout + xpack.security.session.idleTimeout + xpack.security.session.lifespan xpack.security.loginAssistanceMessage telemetry.enabled telemetry.sendUsageFrom diff --git a/src/legacy/core_plugins/status_page/index.js b/src/legacy/core_plugins/status_page/index.js index 34de58048b887..9f0ad632fd5b1 100644 --- a/src/legacy/core_plugins/status_page/index.js +++ b/src/legacy/core_plugins/status_page/index.js @@ -26,6 +26,11 @@ export default function (kibana) { hidden: true, url: '/status', }, + injectDefaultVars(server) { + return { + isStatusPageAnonymous: server.config().get('status.allowAnonymous'), + }; + } } }); } diff --git a/src/plugins/status_page/kibana.json b/src/plugins/status_page/kibana.json new file mode 100644 index 0000000000000..edebf8cb12239 --- /dev/null +++ b/src/plugins/status_page/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "status_page", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/status_page/public/index.ts b/src/plugins/status_page/public/index.ts new file mode 100644 index 0000000000000..db1f05cac076f --- /dev/null +++ b/src/plugins/status_page/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { StatusPagePlugin, StatusPagePluginSetup, StatusPagePluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new StatusPagePlugin(); diff --git a/src/plugins/status_page/public/plugin.ts b/src/plugins/status_page/public/plugin.ts new file mode 100644 index 0000000000000..d072fd4a67c30 --- /dev/null +++ b/src/plugins/status_page/public/plugin.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup } from 'kibana/public'; + +export class StatusPagePlugin implements Plugin { + public setup(core: CoreSetup) { + const isStatusPageAnonymous = core.injectedMetadata.getInjectedVar( + 'isStatusPageAnonymous' + ) as boolean; + + if (isStatusPageAnonymous) { + core.http.anonymousPaths.register('/status'); + } + } + + public start() {} + + public stop() {} +} + +export type StatusPagePluginSetup = ReturnType; +export type StatusPagePluginStart = ReturnType; diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index d147c2572ceeb..60374d562f96c 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -29,7 +29,10 @@ export const security = (kibana) => new kibana.Plugin({ enabled: Joi.boolean().default(true), cookieName: Joi.any().description('This key is handled in the new platform security plugin ONLY'), encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - sessionTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + session: Joi.object({ + idleTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + }).default(), secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), loginAssistanceMessage: Joi.string().default(), authorization: Joi.object({ @@ -44,9 +47,10 @@ export const security = (kibana) => new kibana.Plugin({ }).default(); }, - deprecations: function ({ unused }) { + deprecations: function ({ rename, unused }) { return [ unused('authorization.legacyFallback.enabled'), + rename('sessionTimeout', 'session.idleTimeout'), ]; }, @@ -89,7 +93,11 @@ export const security = (kibana) => new kibana.Plugin({ return { secureCookies: securityPlugin.__legacyCompat.config.secureCookies, - sessionTimeout: securityPlugin.__legacyCompat.config.sessionTimeout, + session: { + tenant: server.newPlatform.setup.core.http.basePath.serverBasePath, + idleTimeout: securityPlugin.__legacyCompat.config.session.idleTimeout, + lifespan: securityPlugin.__legacyCompat.config.session.lifespan, + }, enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'), }; }, diff --git a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js index 81b14ee7d8bf4..d9fb450779411 100644 --- a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js +++ b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js @@ -7,28 +7,20 @@ import _ from 'lodash'; import { uiModules } from 'ui/modules'; import { isSystemApiRequest } from 'ui/system_api'; -import { Path } from 'plugins/xpack_main/services/path'; import { npSetup } from 'ui/new_platform'; -/** - * Client session timeout is decreased by this number so that Kibana server - * can still access session content during logout request to properly clean - * user session up (invalidate access tokens, redirect to logout portal etc.). - * @type {number} - */ - const module = uiModules.get('security', []); module.config(($httpProvider) => { $httpProvider.interceptors.push(( $q, ) => { - const isUnauthenticated = Path.isUnauthenticated(); + const isAnonymous = npSetup.core.http.anonymousPaths.isAnonymous(window.location.pathname); function interceptorFactory(responseHandler) { return function interceptor(response) { - if (!isUnauthenticated && !isSystemApiRequest(response.config)) { - npSetup.plugins.security.sessionTimeout.extend(); + if (!isAnonymous && !isSystemApiRequest(response.config)) { + npSetup.plugins.security.sessionTimeout.extend(response.config.url); } return responseHandler(response); }; diff --git a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx index 369b531e8ddf8..dbeb68875c1a9 100644 --- a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx +++ b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx @@ -31,7 +31,7 @@ chrome } > - + , diff --git a/x-pack/package.json b/x-pack/package.json index bc7b220bf81f5..eccc5918e6d50 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -210,6 +210,7 @@ "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", + "broadcast-channel": "^3.0.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "concat-stream": "1.6.2", diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts index 55d125bf993ec..7b1a554e1d3f1 100644 --- a/x-pack/plugins/security/public/plugin.ts +++ b/x-pack/plugins/security/public/plugin.ts @@ -13,6 +13,8 @@ import { } from './session'; export class SecurityPlugin implements Plugin { + private sessionTimeout!: SessionTimeout; + public setup(core: CoreSetup) { const { http, notifications, injectedMetadata } = core; const { basePath, anonymousPaths } = http; @@ -20,23 +22,25 @@ export class SecurityPlugin implements Plugin; diff --git a/x-pack/plugins/security/public/session/session_expired.test.ts b/x-pack/plugins/security/public/session/session_expired.test.ts index 9c0e4cd8036cc..678c397dfbc64 100644 --- a/x-pack/plugins/security/public/session/session_expired.test.ts +++ b/x-pack/plugins/security/public/session/session_expired.test.ts @@ -7,40 +7,81 @@ import { coreMock } from 'src/core/public/mocks'; import { SessionExpired } from './session_expired'; -const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); - -it('redirects user to "/logout" when there is no basePath', async () => { - const { basePath } = coreMock.createSetup().http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); +describe('Session Expiration', () => { + const mockGetItem = jest.fn().mockReturnValue(null); + + beforeAll(() => { + Object.defineProperty(window, 'sessionStorage', { + value: { + getItem: mockGetItem, + }, + writable: true, }); }); - sessionExpired.logout(); + afterAll(() => { + delete (window as any).sessionStorage; + }); + + describe('logout', () => { + const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); + const tenant = ''; - const url = await newUrlPromise; - expect(url).toBe( - `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); -}); + it('redirects user to "/logout" when there is no basePath', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); -it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { - const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); }); - }); - sessionExpired.logout(); + it('adds a provider parameter when an auth provider is saved in sessionStorage', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + mockGetItem.mockReturnValueOnce('basic'); + + sessionExpired.logout(); - const url = await newUrlPromise; - expect(url).toBe( - `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent( + '/foo/bar?baz=quz#quuz' + )}&msg=SESSION_EXPIRED&provider=basic` + ); + }); + + it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { + const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); + }); + }); }); diff --git a/x-pack/plugins/security/public/session/session_expired.ts b/x-pack/plugins/security/public/session/session_expired.ts index 3ef15088bb288..a43da85526757 100644 --- a/x-pack/plugins/security/public/session/session_expired.ts +++ b/x-pack/plugins/security/public/session/session_expired.ts @@ -11,14 +11,19 @@ export interface ISessionExpired { } export class SessionExpired { - constructor(private basePath: HttpSetup['basePath']) {} + constructor(private basePath: HttpSetup['basePath'], private tenant: string) {} logout() { const next = this.basePath.remove( `${window.location.pathname}${window.location.search}${window.location.hash}` ); + const key = `${this.tenant}/session_provider`; + const providerName = sessionStorage.getItem(key); + const provider = providerName ? `&provider=${encodeURIComponent(providerName)}` : ''; window.location.assign( - this.basePath.prepend(`/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED`) + this.basePath.prepend( + `/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED${provider}` + ) ); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx similarity index 71% rename from x-pack/plugins/security/public/session/session_timeout_warning.test.tsx rename to x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx index a52e7ce4e94b5..bb4116420f15d 100644 --- a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx @@ -5,12 +5,14 @@ */ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { SessionIdleTimeoutWarning } from './session_idle_timeout_warning'; -describe('SessionTimeoutWarning', () => { +describe('SessionIdleTimeoutWarning', () => { it('fires its callback when the OK button is clicked', () => { const handler = jest.fn(); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); expect(handler).toBeCalledTimes(0); wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); diff --git a/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx new file mode 100644 index 0000000000000..32e4dcc5c6b53 --- /dev/null +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiProgress } from '@elastic/eui'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + onRefreshSession: () => void; + timeout: number; +} + +export const SessionIdleTimeoutWarning = (props: Props) => { + return ( + <> + +

    + + ), + }} + /> +

    +
    + + + +
    + + ); +}; + +export const createToast = (toastLifeTimeMs: number, onRefreshSession: () => void): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'warning', + text: toMountPoint( + + ), + title: i18n.translate('xpack.security.components.sessionIdleTimeoutWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'clock', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_lifespan_warning.tsx b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx new file mode 100644 index 0000000000000..7925e92bce4ed --- /dev/null +++ b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { EuiProgress } from '@elastic/eui'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + timeout: number; +} + +export const SessionLifespanWarning = (props: Props) => { + return ( + <> + +

    + + ), + }} + /> +

    + + ); +}; + +export const createToast = (toastLifeTimeMs: number): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'danger', + text: toMountPoint(), + title: i18n.translate('xpack.security.components.sessionLifespanWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'alert', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_timeout.mock.ts b/x-pack/plugins/security/public/session/session_timeout.mock.ts index 9917a50279083..df9b8628b180d 100644 --- a/x-pack/plugins/security/public/session/session_timeout.mock.ts +++ b/x-pack/plugins/security/public/session/session_timeout.mock.ts @@ -8,6 +8,8 @@ import { ISessionTimeout } from './session_timeout'; export function createSessionTimeoutMock() { return { + start: jest.fn(), + stop: jest.fn(), extend: jest.fn(), } as jest.Mocked; } diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index 80a22c5fb0b2a..eb947ab95c43b 100644 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.test.tsx @@ -5,6 +5,7 @@ */ import { coreMock } from 'src/core/public/mocks'; +import BroadcastChannel from 'broadcast-channel'; import { SessionTimeout } from './session_timeout'; import { createSessionExpiredMock } from './session_expired.mock'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -17,25 +18,46 @@ const expectNoWarningToast = ( expect(notifications.toasts.add).not.toHaveBeenCalled(); }; -const expectWarningToast = ( +const expectIdleTimeoutWarningToast = ( notifications: ReturnType['notifications'], - toastLifeTimeMS: number = 60000 + toastLifeTimeMs: number = 60000 ) => { expect(notifications.toasts.add).toHaveBeenCalledTimes(1); - expect(notifications.toasts.add.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "color": "warning", - "text": MountPoint { - "reactNode": , - }, - "title": "Warning", - "toastLifeTimeMs": ${toastLifeTimeMS}, - }, - ] - `); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "warning", + "iconType": "clock", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); +}; + +const expectLifespanWarningToast = ( + notifications: ReturnType['notifications'], + toastLifeTimeMs: number = 60000 +) => { + expect(notifications.toasts.add).toHaveBeenCalledTimes(1); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "danger", + "iconType": "alert", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); }; const expectWarningToastHidden = ( @@ -46,128 +68,309 @@ const expectWarningToastHidden = ( expect(notifications.toasts.remove).toHaveBeenCalledWith(toast); }; -describe('warning toast', () => { - test(`shows session expiration warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); +describe('Session Timeout', () => { + const now = new Date().getTime(); + const defaultSessionInfo = { + now, + idleTimeoutExpiration: now + 2 * 60 * 1000, + lifespanExpiration: null, + }; + let notifications: ReturnType['notifications']; + let http: ReturnType['http']; + let sessionExpired: ReturnType; + let sessionTimeout: SessionTimeout; + const toast = Symbol(); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + beforeAll(() => { + BroadcastChannel.enforceOptions({ + type: 'simulate', + }); + Object.defineProperty(window, 'sessionStorage', { + value: { + setItem: jest.fn(() => null), + }, + writable: true, + }); }); - test(`extend delays the warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + beforeEach(() => { + const setup = coreMock.createSetup(); + notifications = setup.notifications; + http = setup.http; + notifications.toasts.add.mockReturnValue(toast as any); + sessionExpired = createSessionExpiredMock(); + const tenant = ''; + sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + // default mocked response for checking session info + http.fetch.mockResolvedValue(defaultSessionInfo); + }); - jest.advanceTimersByTime(1 * 1000); + afterEach(async () => { + jest.clearAllMocks(); + }); - expectWarningToast(notifications); + afterAll(() => { + BroadcastChannel.enforceOptions(null); + delete (window as any).sessionStorage; }); - test(`extend hides displayed warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const toast = Symbol(); - notifications.toasts.add.mockReturnValue(toast as any); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('Lifecycle', () => { + test(`starts and initializes on a non-anonymous path`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).not.toBeUndefined(); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + test(`starts and does not initialize on an anonymous path`, async () => { + http.anonymousPaths.register(window.location.pathname); + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).toBeUndefined(); + expect(http.fetch).not.toHaveBeenCalled(); + }); - sessionTimeout.extend(); - expectWarningToastHidden(notifications, toast); - }); + test(`stops`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + const close = jest.fn(sessionTimeout['channel']!.close); + // eslint-disable-next-line dot-notation + sessionTimeout['channel']!.close = close; + // eslint-disable-next-line dot-notation + const cleanup = jest.fn(sessionTimeout['cleanup']); + // eslint-disable-next-line dot-notation + sessionTimeout['cleanup'] = cleanup; - test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); - - expect(http.get).not.toHaveBeenCalled(); - const toastInput = notifications.toasts.add.mock.calls[0][0]; - expect(toastInput).toHaveProperty('text'); - const mountPoint = (toastInput as any).text; - const wrapper = mountWithIntl(mountPoint.__reactMount__); - wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); - expect(http.get).toHaveBeenCalled(); + sessionTimeout.stop(); + expect(close).toHaveBeenCalled(); + expect(cleanup).toHaveBeenCalled(); + }); }); - test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(64 * 1000, notifications, sessionExpired, http); + describe('API calls', () => { + const methodName = 'handleSessionInfoAndResetTimers'; + let method: jest.Mock; - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expectWarningToast(notifications, 59 * 1000); - }); -}); + beforeEach(() => { + method = jest.fn(sessionTimeout[methodName]); + sessionTimeout[methodName] = method; + }); -describe('session expiration', () => { - test(`expires the session 5 seconds before it really expires`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + test(`handles success`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBe(defaultSessionInfo); + expect(method).toHaveBeenCalledTimes(1); + }); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`handles error`, async () => { + const mockErrorResponse = new Error('some-error'); + http.fetch.mockRejectedValue(mockErrorResponse); + await sessionTimeout.start(); + + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBeUndefined(); + expect(method).not.toHaveBeenCalled(); + }); }); - test(`extend delays the expiration`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('warning toast', () => { + test(`shows idle timeout warning toast`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectIdleTimeoutWarningToast(notifications); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + test(`shows lifespan warning toast`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); + }); + + test(`extend only results in an HTTP call if a warning is shown`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(54 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectNoWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(10 * 1000); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test(`extend does not result in an HTTP call if a lifespan warning is shown`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); - test(`if the session timeout is shorter than 5 seconds, expire session immediately`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(4 * 1000, notifications, sessionExpired, http); + expect(http.fetch).toHaveBeenCalledTimes(1); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`extend hides displayed warning toast`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expectIdleTimeoutWarningToast(notifications); + + http.fetch.mockResolvedValue({ + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + expectWarningToastHidden(notifications, toast); + }); + + test(`extend does nothing for session-related routes`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/internal/security/session'); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test(`checks for updated session info before the warning displays`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we check for updated session info 1 second before the warning is shown + const elapsed = 54 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const toastInput = notifications.toasts.add.mock.calls[0][0]; + expect(toastInput).toHaveProperty('text'); + const mountPoint = (toastInput as any).text; + const wrapper = mountWithIntl(mountPoint.__reactMount__); + wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 64 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalled(); + + jest.advanceTimersByTime(0); + expectIdleTimeoutWarningToast(notifications, 59 * 1000); + }); }); - test(`'null' sessionTimeout never logs you out`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(null, notifications, sessionExpired, http); - sessionTimeout.extend(); - jest.advanceTimersByTime(Number.MAX_VALUE); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + describe('session expiration', () => { + test(`expires the session 5 seconds before it really expires`, async () => { + await sessionTimeout.start(); + + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1 * 1000); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`extend delays the expiration`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + const elapsed = 114 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const sessionInfo = { + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toEqual(sessionInfo); + + // at this point, the session is good for another 120 seconds + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + // because "extend" results in an async request and HTTP call, there is a slight delay when timers are updated + // so we need an extra 100ms of padding for this test to ensure that logout has been called + jest.advanceTimersByTime(1 * 1000 + 100); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`if the session timeout is shorter than 5 seconds, expire session immediately`, async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 4 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(0); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`'null' sessionTimeout never logs you out`, async () => { + http.fetch.mockResolvedValue({ now, idleTimeoutExpiration: null, lifespanExpiration: null }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(Number.MAX_VALUE); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index 32302effd6e46..0069e78b5f372 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NotificationsSetup, Toast, HttpSetup } from 'src/core/public'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { NotificationsSetup, Toast, HttpSetup, ToastInput } from 'src/core/public'; +import { BroadcastChannel } from 'broadcast-channel'; +import { createToast as createIdleTimeoutToast } from './session_idle_timeout_warning'; +import { createToast as createLifespanToast } from './session_lifespan_warning'; import { ISessionExpired } from './session_expired'; +import { SessionInfo } from '../types'; /** * Client session timeout is decreased by this number so that Kibana server @@ -23,58 +23,188 @@ const GRACE_PERIOD_MS = 5 * 1000; */ const WARNING_MS = 60 * 1000; +/** + * Current session info is checked this number of milliseconds before the + * warning toast shows. This will prevent the toast from being shown if the + * session has already been extended. + */ +const SESSION_CHECK_MS = 1000; + +/** + * Route to get session info and extend session expiration + */ +const SESSION_ROUTE = '/internal/security/session'; + export interface ISessionTimeout { - extend(): void; + start(): void; + stop(): void; + extend(url: string): void; } export class SessionTimeout { - private warningTimeoutMilliseconds?: number; - private expirationTimeoutMilliseconds?: number; + private channel?: BroadcastChannel; + private sessionInfo?: SessionInfo; + private fetchTimer?: number; + private warningTimer?: number; + private expirationTimer?: number; private warningToast?: Toast; constructor( - private sessionTimeoutMilliseconds: number | null, private notifications: NotificationsSetup, private sessionExpired: ISessionExpired, - private http: HttpSetup + private http: HttpSetup, + private tenant: string ) {} - extend() { - if (this.sessionTimeoutMilliseconds == null) { + start() { + if (this.http.anonymousPaths.isAnonymous(window.location.pathname)) { return; } - if (this.warningTimeoutMilliseconds) { - window.clearTimeout(this.warningTimeoutMilliseconds); + // subscribe to a broadcast channel for session timeout messages + // this allows us to synchronize the UX across tabs and avoid repetitive API calls + const name = `${this.tenant}/session_timeout`; + this.channel = new BroadcastChannel(name, { webWorkerSupport: false }); + this.channel.onmessage = this.handleSessionInfoAndResetTimers; + + // Triggers an initial call to the endpoint to get session info; + // when that returns, it will set the timeout + return this.fetchSessionInfoAndResetTimers(); + } + + stop() { + if (this.channel) { + this.channel.close(); } - if (this.expirationTimeoutMilliseconds) { - window.clearTimeout(this.expirationTimeoutMilliseconds); + this.cleanup(); + } + + /** + * When the user makes an authenticated, non-system API call, this function is used to check + * and see if the session has been extended. + * @param url The URL that was called + */ + extend(url: string) { + // avoid an additional API calls when the user clicks the button on the session idle timeout + if (url.endsWith(SESSION_ROUTE)) { + return; } - if (this.warningToast) { - this.notifications.toasts.remove(this.warningToast); + + const { isLifespanTimeout } = this.getTimeout(); + if (this.warningToast && !isLifespanTimeout) { + // the idle timeout warning is currently showing and the user has clicked elsewhere on the page; + // make a new call to get the latest session info + return this.fetchSessionInfoAndResetTimers(); + } + } + + /** + * Fetch latest session information from the server, and optionally attempt to extend + * the session expiration. + */ + private fetchSessionInfoAndResetTimers = async (extend = false) => { + const method = extend ? 'POST' : 'GET'; + const headers = extend ? {} : { 'kbn-system-api': 'true' }; + try { + const result = await this.http.fetch(SESSION_ROUTE, { method, headers }); + + this.handleSessionInfoAndResetTimers(result); + + // share this updated session info with any other tabs to sync the UX + if (this.channel) { + this.channel.postMessage(result); + } + } catch (err) { + // do nothing; 401 errors will be caught by the http interceptor + } + }; + + /** + * Processes latest session information, and resets timers based on it. These timers are + * used to trigger an HTTP call for updated session information, to show a timeout + * warning, and to log the user out when their session is expired. + */ + private handleSessionInfoAndResetTimers = (sessionInfo: SessionInfo) => { + this.sessionInfo = sessionInfo; + // save the provider name in session storage, we will need it when we log out + const key = `${this.tenant}/session_provider`; + sessionStorage.setItem(key, sessionInfo.provider); + + const { timeout, isLifespanTimeout } = this.getTimeout(); + if (timeout == null) { + return; } - this.warningTimeoutMilliseconds = window.setTimeout( - () => this.showWarning(), - Math.max(this.sessionTimeoutMilliseconds - WARNING_MS - GRACE_PERIOD_MS, 0) + + this.cleanup(); + + // set timers + const timeoutVal = timeout - WARNING_MS - GRACE_PERIOD_MS - SESSION_CHECK_MS; + if (timeoutVal > 0 && !isLifespanTimeout) { + // we should check for the latest session info before the warning displays + this.fetchTimer = window.setTimeout(this.fetchSessionInfoAndResetTimers, timeoutVal); + } + this.warningTimer = window.setTimeout( + this.showWarning, + Math.max(timeout - WARNING_MS - GRACE_PERIOD_MS, 0) ); - this.expirationTimeoutMilliseconds = window.setTimeout( + this.expirationTimer = window.setTimeout( () => this.sessionExpired.logout(), - Math.max(this.sessionTimeoutMilliseconds - GRACE_PERIOD_MS, 0) + Math.max(timeout - GRACE_PERIOD_MS, 0) ); - } + }; - private showWarning = () => { - this.warningToast = this.notifications.toasts.add({ - color: 'warning', - text: toMountPoint(), - title: i18n.translate('xpack.security.components.sessionTimeoutWarning.title', { - defaultMessage: 'Warning', - }), - toastLifeTimeMs: Math.min(this.sessionTimeoutMilliseconds! - GRACE_PERIOD_MS, WARNING_MS), - }); + private cleanup = () => { + if (this.fetchTimer) { + window.clearTimeout(this.fetchTimer); + } + if (this.warningTimer) { + window.clearTimeout(this.warningTimer); + } + if (this.expirationTimer) { + window.clearTimeout(this.expirationTimer); + } + if (this.warningToast) { + this.notifications.toasts.remove(this.warningToast); + this.warningToast = undefined; + } }; - private refreshSession = () => { - this.http.get('/api/security/v1/me'); + /** + * Get the amount of time until the session times out, and whether or not the + * session has reached it maximum lifespan. + */ + private getTimeout = (): { timeout: number | null; isLifespanTimeout: boolean } => { + let timeout = null; + let isLifespanTimeout = false; + if (this.sessionInfo) { + const { now, idleTimeoutExpiration, lifespanExpiration } = this.sessionInfo; + if (idleTimeoutExpiration) { + timeout = idleTimeoutExpiration - now; + } + if ( + lifespanExpiration && + (idleTimeoutExpiration === null || lifespanExpiration <= idleTimeoutExpiration) + ) { + timeout = lifespanExpiration - now; + isLifespanTimeout = true; + } + } + return { timeout, isLifespanTimeout }; + }; + + /** + * Show a warning toast depending on the session state. + */ + private showWarning = () => { + const { timeout, isLifespanTimeout } = this.getTimeout(); + const toastLifeTimeMs = Math.min(timeout! - GRACE_PERIOD_MS, WARNING_MS); + let toast: ToastInput; + if (!isLifespanTimeout) { + const refresh = () => this.fetchSessionInfoAndResetTimers(true); + toast = createIdleTimeoutToast(toastLifeTimeMs, refresh); + } else { + toast = createLifespanToast(toastLifeTimeMs); + } + this.warningToast = this.notifications.toasts.add(toast); }; } diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts index 98516cb4a613b..81625e1753b27 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts @@ -24,7 +24,7 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpResponse.request.url); } responseError(httpErrorResponse: HttpErrorResponse) { @@ -45,6 +45,6 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpErrorResponse.request.url); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_timeout_warning.tsx deleted file mode 100644 index e1b4542031ed1..0000000000000 --- a/x-pack/plugins/security/public/session/session_timeout_warning.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiButton } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface Props { - onRefreshSession: () => void; -} - -export const SessionTimeoutWarning = (props: Props) => { - return ( - <> -

    - -

    -
    - - - -
    - - ); -}; diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 6f339a6fc9c95..ff2db01cb6c58 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -25,6 +25,7 @@ const setupHttp = (basePath: string) => { }); return http; }; +const tenant = ''; afterEach(() => { fetchMock.restore(); @@ -32,7 +33,7 @@ afterEach(() => { it(`logs out 401 responses`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const logoutPromise = new Promise(resolve => { jest.spyOn(sessionExpired, 'logout').mockImplementation(() => resolve()); }); @@ -58,7 +59,7 @@ it(`ignores anonymous paths`, async () => { const http = setupHttp('/foo'); const { anonymousPaths } = http; anonymousPaths.register('/bar'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); @@ -69,7 +70,7 @@ it(`ignores anonymous paths`, async () => { it(`ignores errors which don't have a response, for example network connectivity issues`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', new Promise((resolve, reject) => reject(new Error('Network is down')))); @@ -80,7 +81,7 @@ it(`ignores errors which don't have a response, for example network connectivity it(`ignores requests which omit credentials`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); diff --git a/x-pack/plugins/security/public/types.ts b/x-pack/plugins/security/public/types.ts new file mode 100644 index 0000000000000..e9c4b6e281cf3 --- /dev/null +++ b/x-pack/plugins/security/public/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SessionInfo { + now: number; + idleTimeoutExpiration: number | null; + lifespanExpiration: number | null; + provider: string; +} diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 78c6feac0fa29..12b4620d554a2 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -28,7 +28,11 @@ function getMockOptions(config: Partial = {}) { basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), isSystemAPIRequest: jest.fn(), - config: { sessionTimeout: null, authc: { providers: [], oidc: {}, saml: {} }, ...config }, + config: { + session: { idleTimeout: null, lifespan: null }, + authc: { providers: [], oidc: {}, saml: {} }, + ...config, + }, sessionStorageFactory: sessionStorageMock.createFactory(), }; } @@ -51,7 +55,9 @@ describe('Authenticator', () => { describe('initialization', () => { it('fails if authentication providers are not configured.', () => { - const mockOptions = getMockOptions({ authc: { providers: [], oidc: {}, saml: {} } }); + const mockOptions = getMockOptions({ + authc: { providers: [], oidc: {}, saml: {} }, + }); expect(() => new Authenticator(mockOptions)).toThrowError( 'No authentication provider is configured. Verify `xpack.security.authc.providers` config value.' ); @@ -73,7 +79,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -151,7 +159,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -173,7 +182,12 @@ describe('Authenticator', () => { const request = httpServerMock.createKibanaRequest(); mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user)); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.login(request, { provider: 'basic', @@ -286,7 +300,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -344,7 +360,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -366,7 +383,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -381,7 +399,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -400,7 +423,58 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + + it('properly extends session expiration if it is defined.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + + // Create new authenticator with non-null session `idleTimeout`. + mockOptions = getMockOptions({ + session: { + idleTimeout: 3600 * 24, + lifespan: null, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -408,27 +482,39 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: currentDate + 3600 * 24, + lifespanExpiration: null, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); - it('properly extends session timeout if it is defined.', async () => { + it('does not extend session lifespan expiration.', async () => { const user = mockAuthenticatedUser(); const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const hr = 1000 * 60 * 60; - // Create new authenticator with non-null `sessionTimeout`. + // Create new authenticator with non-null session `idleTimeout` and `lifespan`. mockOptions = getMockOptions({ - sessionTimeout: 3600 * 24, + session: { + idleTimeout: hr * 2, + lifespan: hr * 8, + }, authc: { providers: ['basic'], oidc: {}, saml: {} }, }); mockSessionStorage = sessionStorageMock.create(); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + // this session was created 6.5 hrs ago (and has 1.5 hrs left in its lifespan) + // it was last extended 1 hour ago, which means it will expire in 1 hour + idleTimeoutExpiration: currentDate + hr * 1, + lifespanExpiration: currentDate + hr * 1.5, + state, + provider: 'basic', + }); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); authenticator = new Authenticator(mockOptions); @@ -445,13 +531,69 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: currentDate + 3600 * 24, + idleTimeoutExpiration: currentDate + hr * 2, + lifespanExpiration: currentDate + hr * 1.5, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); + it('only updates the session lifespan expiration if it does not match the current server config.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const hr = 1000 * 60 * 60; + + async function createAndUpdateSession( + lifespan: number | null, + oldExpiration: number | null, + newExpiration: number | null + ) { + mockOptions = getMockOptions({ + session: { + idleTimeout: null, + lifespan, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: 1, + lifespanExpiration: oldExpiration, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: 1, + lifespanExpiration: newExpiration, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + } + // do not change max expiration + createAndUpdateSession(hr * 8, 1234, 1234); + createAndUpdateSession(null, null, null); + // change max expiration + createAndUpdateSession(null, 1234, null); + createAndUpdateSession(hr * 8, null, hr * 8); + }); + it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); @@ -460,7 +602,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -477,7 +624,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -497,7 +649,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -508,7 +661,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -526,7 +680,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -537,7 +692,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -552,7 +708,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -569,7 +730,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -585,7 +751,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.redirectTo('some-url', { state: null }) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.redirected()).toBe(true); @@ -602,7 +773,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -619,7 +795,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -636,7 +817,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -653,7 +839,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -668,7 +859,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -697,7 +890,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.logout.mockResolvedValue( DeauthenticationResult.redirectTo('some-url') ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const deauthenticationResult = await authenticator.logout(request); @@ -707,10 +905,41 @@ describe('Authenticator', () => { expect(deauthenticationResult.redirectURL).toBe('some-url'); }); + it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic' } }); + mockSessionStorage.get.mockResolvedValue(null); + + mockBasicAuthenticationProvider.logout.mockResolvedValue( + DeauthenticationResult.redirectTo('some-url') + ); + + const deauthenticationResult = await authenticator.logout(request); + + expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + expect(deauthenticationResult.redirected()).toBe(true); + expect(deauthenticationResult.redirectURL).toBe('some-url'); + }); + + it('returns `notHandled` if session does not exist and provider name is invalid', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'foo' } }); + mockSessionStorage.get.mockResolvedValue(null); + + const deauthenticationResult = await authenticator.logout(request); + + expect(deauthenticationResult.notHandled()).toBe(true); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + it('only clears session if it belongs to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); const state = { authorization: 'Bearer xxx' }; - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const deauthenticationResult = await authenticator.logout(request); @@ -719,4 +948,51 @@ describe('Authenticator', () => { expect(deauthenticationResult.notHandled()).toBe(true); }); }); + + describe('`getSessionInfo` method', () => { + let authenticator: Authenticator; + let mockOptions: ReturnType; + let mockSessionStorage: jest.Mocked>; + beforeEach(() => { + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + mockSessionStorage = sessionStorageMock.create(); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + }); + + it('returns current session info if session exists.', async () => { + const request = httpServerMock.createKibanaRequest(); + const state = { authorization: 'Basic xxx' }; + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const mockInfo = { + now: currentDate, + idleTimeoutExpiration: currentDate + 60000, + lifespanExpiration: currentDate + 120000, + provider: 'basic', + }; + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: mockInfo.idleTimeoutExpiration, + lifespanExpiration: mockInfo.lifespanExpiration, + state, + provider: mockInfo.provider, + }); + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toEqual(mockInfo); + }); + + it('returns `null` if session does not exist.', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(null); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toBe(null); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 18bdc9624b12b..17a773c6b6e8c 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -31,6 +31,7 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; +import { SessionInfo } from '../../public/types'; /** * The shape of the session that is actually stored in the cookie. @@ -45,7 +46,13 @@ export interface ProviderSession { * The Unix time in ms when the session should be considered expired. If `null`, session will stay * active until the browser is closed. */ - expires: number | null; + idleTimeoutExpiration: number | null; + + /** + * The Unix time in ms which is the max total lifespan of the session. If `null`, session expire + * time can be extended indefinitely. + */ + lifespanExpiration: number | null; /** * Session value that is fed to the authentication provider. The shape is unknown upfront and @@ -77,7 +84,7 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { - config: Pick; + config: Pick; basePath: HttpServiceSetup['basePath']; loggers: LoggerFactory; clusterClient: IClusterClient; @@ -153,9 +160,14 @@ export class Authenticator { private readonly providers: Map; /** - * Session duration in ms. If `null` session will stay active until the browser is closed. + * Session timeout in ms. If `null` session will stay active until the browser is closed. */ - private readonly ttl: number | null = null; + private readonly idleTimeout: number | null = null; + + /** + * Session max lifespan in ms. If `null` session may live indefinitely. + */ + private readonly lifespan: number | null = null; /** * Internal authenticator logger. @@ -202,7 +214,9 @@ export class Authenticator { }) ); - this.ttl = this.options.config.sessionTimeout; + // only set these vars if they are defined in options (otherwise coalesce to existing/default) + this.idleTimeout = this.options.config.session.idleTimeout; + this.lifespan = this.options.config.session.lifespan; } /** @@ -257,10 +271,12 @@ export class Authenticator { if (existingSession && shouldClearSession) { sessionStorage.clear(); } else if (!attempt.stateless && authenticationResult.shouldUpdateState()) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.state, provider: attempt.provider, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } @@ -315,10 +331,18 @@ export class Authenticator { const sessionStorage = this.options.sessionStorageFactory.asScoped(request); const sessionValue = await this.getSessionValue(sessionStorage); + const providerName = this.getProviderName(request.query); if (sessionValue) { sessionStorage.clear(); return this.providers.get(sessionValue.provider)!.logout(request, sessionValue.state); + } else if (providerName) { + // provider name is passed in a query param and sourced from the browser's local storage; + // hence, we can't assume that this provider exists, so we have to check it + const provider = this.providers.get(providerName); + if (provider) { + return provider.logout(request, null); + } } // Normally when there is no active session in Kibana, `logout` method shouldn't do anything @@ -334,6 +358,29 @@ export class Authenticator { return DeauthenticationResult.notHandled(); } + /** + * Returns session information for the current request. + * @param request Request instance. + */ + async getSessionInfo(request: KibanaRequest): Promise { + assertRequest(request); + + const sessionStorage = this.options.sessionStorageFactory.asScoped(request); + const sessionValue = await this.getSessionValue(sessionStorage); + + if (sessionValue) { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + return { + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + }; + } + return null; + } + /** * Returns provider iterator where providers are sorted in the order of priority (based on the session ownership). * @param sessionValue Current session value. @@ -410,13 +457,34 @@ export class Authenticator { ) { sessionStorage.clear(); } else if (sessionCanBeUpdated) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.shouldUpdateState() ? authenticationResult.state : existingSession!.state, provider: providerType, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } } + + private getProviderName(query: any): string | null { + if (query && query.provider && typeof query.provider === 'string') { + return query.provider; + } + return null; + } + + private calculateExpiry( + existingSession: ProviderSession | null + ): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } { + let lifespanExpiration = this.lifespan && Date.now() + this.lifespan; + if (existingSession && existingSession.lifespanExpiration && this.lifespan) { + lifespanExpiration = existingSession.lifespanExpiration; + } + const idleTimeoutExpiration = this.idleTimeout && Date.now() + this.idleTimeout; + + return { idleTimeoutExpiration, lifespanExpiration }; + } } diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index dcaf26f53fe01..77f1f9e45aea7 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -14,5 +14,6 @@ export const authenticationMock = { invalidateAPIKey: jest.fn(), isAuthenticated: jest.fn(), logout: jest.fn(), + getSessionInfo: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index df16dd375e858..2e67a0eaaa6d5 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -68,15 +68,22 @@ export async function setupAuthentication({ const authenticator = new Authenticator({ clusterClient, basePath: http.basePath, - config: { sessionTimeout: config.sessionTimeout, authc: config.authc }, + config: { session: config.session, authc: config.authc }, isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, isSecure: config.secureCookies, name: config.cookieName, - validate: (sessionValue: ProviderSession) => - !(sessionValue.expires && sessionValue.expires < Date.now()), + validate: (sessionValue: ProviderSession) => { + const { idleTimeoutExpiration, lifespanExpiration } = sessionValue; + if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) { + return false; + } else if (lifespanExpiration && lifespanExpiration < Date.now()) { + return false; + } + return true; + }, }), }); @@ -151,6 +158,7 @@ export async function setupAuthentication({ return { login: authenticator.login.bind(authenticator), logout: authenticator.logout.bind(authenticator), + getSessionInfo: authenticator.getSessionInfo.bind(authenticator), getCurrentUser, createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 8eb20447c7e2c..a6850dcdf8321 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -422,20 +422,16 @@ describe('TokenAuthenticationProvider', () => { }); describe('`logout` method', () => { - it('returns `notHandled` if state is not presented.', async () => { + it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); sinon.assert.notCalled(mockOptions.tokens.invalidate); - - deauthenticateResult = await provider.logout(request, tokenPair); - expect(deauthenticateResult.notHandled()).toBe(false); }); it('fails if `tokens.invalidate` fails', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index d1881ad4b5498..c5f8f07e50b11 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -120,18 +120,16 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { public async logout(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to log user out via ${request.url.path}.`); - if (!state) { + if (state) { + this.logger.debug('Token-based logout has been initiated by the user.'); + try { + await this.options.tokens.invalidate(state); + } catch (err) { + this.logger.debug(`Failed invalidating user's access token: ${err.message}`); + return DeauthenticationResult.failed(err); + } + } else { this.logger.debug('There are no access and refresh tokens to invalidate.'); - return DeauthenticationResult.notHandled(); - } - - this.logger.debug('Token-based logout has been initiated by the user.'); - - try { - await this.options.tokens.invalidate(state); - } catch (err) { - this.logger.debug(`Failed invalidating user's access token: ${err.message}`); - return DeauthenticationResult.failed(err); } const queryString = request.url.search || `?msg=LOGGED_OUT`; diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 569611516c880..9ddb3e6e96b90 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -13,48 +13,57 @@ import { createConfig$, ConfigSchema } from './config'; describe('config schema', () => { it('generates proper defaults', () => { expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { @@ -253,7 +262,11 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16), secureCookies: true }); + expect(config).toEqual({ + encryptionKey: 'ab'.repeat(16), + secureCookies: true, + session: { idleTimeout: null, lifespan: null }, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -273,7 +286,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: false }); + expect(config.secureCookies).toEqual(false); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -293,7 +306,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -313,7 +326,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index a257a25344393..c7d990f81369e 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -34,7 +34,11 @@ export const ConfigSchema = schema.object( schema.maybe(schema.string({ minLength: 32 })), schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) ), - sessionTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + sessionTimeout: schema.maybe(schema.oneOf([schema.number(), schema.literal(null)])), // DEPRECATED + session: schema.object({ + idleTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + lifespan: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + }), secureCookies: schema.boolean({ defaultValue: false }), authc: schema.object({ providers: schema.arrayOf(schema.string(), { defaultValue: ['basic'], minSize: 1 }), @@ -83,11 +87,23 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b secureCookies = true; } - return { + // "sessionTimeout" is deprecated and replaced with "session.idleTimeout" + // however, NP does not yet have a mechanism to automatically rename deprecated keys + // for the time being, we'll do it manually: + const sess = config.session; + const session = { + idleTimeout: (sess && sess.idleTimeout) || config.sessionTimeout || null, + lifespan: (sess && sess.lifespan) || null, + }; + + const val = { ...config, encryptionKey, secureCookies, + session, }; + delete val.sessionTimeout; // DEPRECATED + return val; }) ); } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 2ff0e915fc1b0..26788c3ef9230 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -20,7 +20,10 @@ describe('Security Plugin', () => { plugin = new Plugin( coreMock.createPluginInitializerContext({ cookieName: 'sid', - sessionTimeout: 1500, + session: { + idleTimeout: 1500, + lifespan: null, + }, authc: { providers: ['saml', 'token'], saml: { realm: 'saml1', maxRedirectURLSize: new ByteSizeValue(2048) }, @@ -54,7 +57,10 @@ describe('Security Plugin', () => { "cookieName": "sid", "loginAssistanceMessage": undefined, "secureCookies": true, - "sessionTimeout": 1500, + "session": Object { + "idleTimeout": 1500, + "lifespan": null, + }, }, "license": Object { "getFeatures": [Function], @@ -66,6 +72,7 @@ describe('Security Plugin', () => { "authc": Object { "createAPIKey": [Function], "getCurrentUser": [Function], + "getSessionInfo": [Function], "invalidateAPIKey": [Function], "isAuthenticated": [Function], "login": [Function], diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index c8761050524a5..e956603517349 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -74,7 +74,10 @@ export interface PluginSetupContract { registerPrivilegesWithCluster: () => void; license: SecurityLicense; config: RecursiveReadonly<{ - sessionTimeout: number | null; + session: { + idleTimeout: number | null; + lifespan: number | null; + }; secureCookies: boolean; authc: { providers: string[] }; }>; @@ -206,7 +209,10 @@ export class Plugin { // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { loginAssistanceMessage: config.loginAssistanceMessage, - sessionTimeout: config.sessionTimeout, + session: { + idleTimeout: config.session.idleTimeout, + lifespan: config.session.lifespan, + }, secureCookies: config.secureCookies, cookieName: config.cookieName, authc: { providers: config.authc.providers }, diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 0e3f03255dcb9..086647dcb3459 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { defineSessionRoutes } from './session'; import { defineSAMLRoutes } from './saml'; import { RouteDefinitionParams } from '..'; export function defineAuthenticationRoutes(params: RouteDefinitionParams) { + defineSessionRoutes(params); if (params.config.authc.providers.includes('saml')) { defineSAMLRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/authentication/session.ts b/x-pack/plugins/security/server/routes/authentication/session.ts new file mode 100644 index 0000000000000..cdebc19d7cf8d --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/session.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for all authentication realms. + */ +export function defineSessionRoutes({ router, logger, authc, basePath }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, request, response) => { + try { + const sessionInfo = await authc.getSessionInfo(request); + // This is an authenticated request, so sessionInfo will always be non-null. + return response.ok({ body: sessionInfo! }); + } catch (err) { + logger.error(`Error retrieving user session: ${err.message}`); + return response.internalError(); + } + } + ); + + router.post( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, _request, response) => { + // We can't easily return updated session info in a single HTTP call, because session data is obtained from + // the HTTP request, not the response. So the easiest way to facilitate this is to redirect the client to GET + // the session endpoint after the client's session has been extended. + return response.redirected({ + headers: { + location: `${basePath.serverBasePath}/internal/security/session`, + }, + }); + } + ); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f83d0c9ea3c9a..f5fc453557122 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9929,9 +9929,8 @@ "xpack.security.account.passwordsDoNotMatch": "パスワードが一致していません。", "xpack.security.account.usernameGroupDescription": "この情報は変更できません。", "xpack.security.account.usernameGroupTitle": "ユーザー名とメールアドレス", - "xpack.security.components.sessionTimeoutWarning.message": "操作がないため間もなくログアウトします。再開するには [OK] をクリックしてくださ。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "OK", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "OK", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "ログイン", "xpack.security.loggedOut.title": "ログアウト完了", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "無効なユーザー名またはパスワード再試行してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a830eaacd29e3..288fc92be3cbd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10018,9 +10018,8 @@ "xpack.security.account.passwordsDoNotMatch": "密码不匹配。", "xpack.security.account.usernameGroupDescription": "不能更改此信息。", "xpack.security.account.usernameGroupTitle": "用户名和电子邮件", - "xpack.security.components.sessionTimeoutWarning.message": "由于处于不活动状态,您即将退出。单击“确定”可以恢复。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "确定", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "确定", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "登录", "xpack.security.loggedOut.title": "已成功退出", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "用户名或密码无效。请重试。", diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index 4d034622427fc..052d984774e69 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./session')); }); } diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts new file mode 100644 index 0000000000000..7c7883f58cb30 --- /dev/null +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Cookie, cookie } from 'request'; +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const config = getService('config'); + + const kibanaServerConfig = config.get('servers.kibana'); + const validUsername = kibanaServerConfig.username; + const validPassword = kibanaServerConfig.password; + + describe('Session', () => { + let sessionCookie: Cookie; + + const saveCookie = async (response: any) => { + // save the response cookie, and pass back the result + sessionCookie = cookie(response.headers['set-cookie'][0])!; + return response; + }; + const getSessionInfo = async () => + supertest + .get('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('kbn-system-api', 'true') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(200); + const extendSession = async () => + supertest + .post('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(302) + .then(saveCookie); + + beforeEach(async () => { + await supertest + .post('/api/security/v1/login') + .set('kbn-xsrf', 'xxx') + .send({ username: validUsername, password: validPassword }) + .expect(204) + .then(saveCookie); + }); + + describe('GET /internal/security/session', () => { + it('should return current session information', async () => { + const { body } = await getSessionInfo(); + expect(body.now).to.be.a('number'); + expect(body.idleTimeoutExpiration).to.be.a('number'); + expect(body.lifespanExpiration).to.be(null); + expect(body.provider).to.be('basic'); + }); + + it('should not extend the session', async () => { + const { body } = await getSessionInfo(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.equal(body.idleTimeoutExpiration); + }); + }); + + describe('POST /internal/security/session', () => { + it('should redirect to GET', async () => { + const response = await extendSession(); + expect(response.headers.location).to.be('/internal/security/session'); + }); + + it('should extend the session', async () => { + // browsers will follow the redirect and return the new session info, but this testing framework does not + // we simulate that behavior in this test by sending another GET request + const { body } = await getSessionInfo(); + await extendSession(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.be.greaterThan(body.idleTimeoutExpiration); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 64a9cafca406a..9c67dfe61b957 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -21,6 +21,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { ...xPackFunctionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', ], }, diff --git a/yarn.lock b/yarn.lock index e30abf76145a3..7e965979fd46f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1004,7 +1004,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3": +"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== @@ -6448,6 +6448,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +big-integer@^1.6.16: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + big-time@2.x.x: version "2.0.1" resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" @@ -6779,6 +6784,19 @@ brfs@^2.0.2: static-module "^3.0.2" through2 "^2.0.0" +broadcast-channel@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.0.3.tgz#e6668693af410f7dda007fd6f80e21992d51f3cc" + integrity sha512-ogRIiGDL0bdeOzPO13YQKX12IvRBDOxej2CJaEwuEOF011C9JBABz+8MJ/WZ34eGbXGrfVBeeeaMTWjBzxVKkw== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.0.4" + js-sha3 "0.8.0" + microseconds "0.1.0" + nano-time "1.0.0" + rimraf "3.0.0" + unload "2.2.0" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -16938,6 +16956,11 @@ js-levenshtein@^1.1.3: resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.3.tgz#3ef627df48ec8cf24bacf05c0f184ff30ef413c5" integrity sha512-/812MXr9RBtMObviZ8gQBhHO8MOrGj8HlEE+4ccMTElNA/6I3u39u+bhny55Lk921yn44nSZFy9naNLElL5wgQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" @@ -18997,6 +19020,11 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +microseconds@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.1.0.tgz#47dc7bcf62171b8030e2152fd82f12a6894a7119" + integrity sha1-R9x7z2IXG4Aw4hUv2C8SpolKcRk= + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -19577,6 +19605,13 @@ nan@^2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= + dependencies: + big-integer "^1.6.16" + nanomatch@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" @@ -24453,6 +24488,13 @@ rimraf@2.6.3, rimraf@^2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" +rimraf@3.0.0, rimraf@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" + integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== + dependencies: + glob "^7.1.3" + rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -24460,13 +24502,6 @@ rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" - integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== - dependencies: - glob "^7.1.3" - rimraf@~2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.0.3.tgz#f50a2965e7144e9afd998982f15df706730f56a9" @@ -28185,6 +28220,14 @@ unlazy-loader@^0.1.3: dependencies: requires-regex "^0.3.3" +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" From 1fb779b7d03a4bc970f1d987080b524d23fdec52 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Tue, 26 Nov 2019 11:34:03 -0500 Subject: [PATCH 091/128] Skipped these tests because their apps are not enabled on cloud. (#51677) --- x-pack/test/functional/apps/cross_cluster_replication/index.ts | 2 +- x-pack/test/functional/apps/remote_clusters/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 21fc1982edc42..efcfaaba6037c 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index dc47bd9de3815..9a4bc5b6a5cbd 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; From 074f24ee32c5ef59c49eb89856e0502eb15f3d75 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 26 Nov 2019 18:03:16 +0100 Subject: [PATCH 092/128] [APM] Fix watcher integration (#51721) Closes #51720. --- .../ServiceIntegrations/WatcherFlyout.tsx | 1 + .../__test__/createErrorGroupWatch.test.ts | 20 +++++++++++++------ .../createErrorGroupWatch.ts | 10 ++++++++-- .../apm/public/services/rest/callApi.ts | 7 +++---- .../services/rest/{watcher.js => watcher.ts} | 15 +++++++++++--- 5 files changed, 38 insertions(+), 15 deletions(-) rename x-pack/legacy/plugins/apm/public/services/rest/{watcher.js => watcher.ts} (60%) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index d52c869b95872..18964531958f7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -191,6 +191,7 @@ export class WatcherFlyout extends Component< ) as string; return createErrorGroupWatch({ + http: core.http, emails, schedule, serviceName, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index c7860b81a7b1e..f05d343ad7ba5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -7,22 +7,30 @@ import { isArray, isObject, isString } from 'lodash'; import mustache from 'mustache'; import uuid from 'uuid'; -// @ts-ignore import * as rest from '../../../../../services/rest/watcher'; import { createErrorGroupWatch } from '../createErrorGroupWatch'; import { esResponse } from './esResponse'; +import { HttpServiceBase } from 'kibana/public'; // disable html escaping since this is also disabled in watcher\s mustache implementation mustache.escape = value => value; +jest.mock('../../../../../services/rest/callApi', () => ({ + callApi: () => Promise.resolve(null) +})); + describe('createErrorGroupWatch', () => { let createWatchResponse: string; let tmpl: any; + const createWatchSpy = jest + .spyOn(rest, 'createWatch') + .mockResolvedValue(undefined); + beforeEach(async () => { jest.spyOn(uuid, 'v4').mockReturnValue(new Buffer('mocked-uuid')); - jest.spyOn(rest, 'createWatch').mockReturnValue(undefined); createWatchResponse = await createErrorGroupWatch({ + http: {} as HttpServiceBase, emails: ['my@email.dk', 'mySecond@email.dk'], schedule: { daily: { @@ -36,19 +44,19 @@ describe('createErrorGroupWatch', () => { apmIndexPatternTitle: 'myIndexPattern' }); - const watchBody = rest.createWatch.mock.calls[0][1]; + const watchBody = createWatchSpy.mock.calls[0][0].watch; const templateCtx = { payload: esResponse, metadata: watchBody.metadata }; - tmpl = renderMustache(rest.createWatch.mock.calls[0][1], templateCtx); + tmpl = renderMustache(createWatchSpy.mock.calls[0][0].watch, templateCtx); }); afterEach(() => jest.restoreAllMocks()); it('should call createWatch with correct args', () => { - expect(rest.createWatch.mock.calls[0][0]).toBe('apm-mocked-uuid'); + expect(createWatchSpy.mock.calls[0][0].id).toBe('apm-mocked-uuid'); }); it('should format slack message correctly', () => { @@ -78,7 +86,7 @@ describe('createErrorGroupWatch', () => { }); it('should return watch id', async () => { - const id = rest.createWatch.mock.calls[0][0]; + const id = createWatchSpy.mock.calls[0][0].id; expect(createWatchResponse).toEqual(id); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index e7d06403b8f8e..1d21e35f122d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import url from 'url'; import uuid from 'uuid'; +import { HttpServiceBase } from 'kibana/public'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -17,7 +18,6 @@ import { PROCESSOR_EVENT, SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; -// @ts-ignore import { createWatch } from '../../../../services/rest/watcher'; function getSlackPathUrl(slackUrl?: string) { @@ -35,6 +35,7 @@ export interface Schedule { } interface Arguments { + http: HttpServiceBase; emails: string[]; schedule: Schedule; serviceName: string; @@ -54,6 +55,7 @@ interface Actions { } export async function createErrorGroupWatch({ + http, emails = [], schedule, serviceName, @@ -250,6 +252,10 @@ export async function createErrorGroupWatch({ }; } - await createWatch(id, body); + await createWatch({ + http, + id, + watch: body + }); return id; } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index e1b61d06e3559..887200bdfc22a 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -9,10 +9,11 @@ import LRU from 'lru-cache'; import hash from 'object-hash'; import { HttpServiceBase, HttpFetchOptions } from 'kibana/public'; -export type FetchOptions = HttpFetchOptions & { +export type FetchOptions = Omit & { pathname: string; forceCache?: boolean; method?: string; + body?: any; }; function fetchOptionsWithDebug(fetchOptions: FetchOptions) { @@ -26,9 +27,7 @@ function fetchOptionsWithDebug(fetchOptions: FetchOptions) { const body = isGet ? {} : { - body: JSON.stringify( - fetchOptions.body || ({} as HttpFetchOptions['body']) - ) + body: JSON.stringify(fetchOptions.body || {}) }; return { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts similarity index 60% rename from x-pack/legacy/plugins/apm/public/services/rest/watcher.js rename to x-pack/legacy/plugins/apm/public/services/rest/watcher.ts index 9d68a1665912c..dfa64b5368ee9 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js +++ b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpServiceBase } from 'kibana/public'; import { callApi } from './callApi'; -export async function createWatch(id, watch) { - return callApi({ +export async function createWatch({ + id, + watch, + http +}: { + http: HttpServiceBase; + id: string; + watch: any; +}) { + return callApi(http, { method: 'PUT', pathname: `/api/watcher/watch/${id}`, - body: JSON.stringify({ type: 'json', id, watch }) + body: { type: 'json', id, watch } }); } From c8f0a751a775b2dbe1f8f26629ed00326a4a3479 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 26 Nov 2019 12:20:37 -0500 Subject: [PATCH 093/128] [Timepicker] Ensure we filter out undefined values (#51458) * Fix error with undefined from or to * PR feedback * Remove unnecessary test --- src/plugins/data/public/query/timefilter/time_history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/query/timefilter/time_history.ts b/src/plugins/data/public/query/timefilter/time_history.ts index 4dabbb557e9db..fe73fd85b164d 100644 --- a/src/plugins/data/public/query/timefilter/time_history.ts +++ b/src/plugins/data/public/query/timefilter/time_history.ts @@ -37,7 +37,7 @@ export class TimeHistory { } add(time: TimeRange) { - if (!time) { + if (!time || !time.from || !time.to) { return; } From 84489619bbc1f7d0e0d6104e0248116460074227 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 26 Nov 2019 18:21:02 +0100 Subject: [PATCH 094/128] Upgrade typescript-eslint to 2.9.0 (#51737) * Upgrade typescript-eslint to 2.9.0 * Remove redundant APM eslint disable --- package.json | 4 +- packages/eslint-config-kibana/package.json | 4 +- .../avg_duration_by_browser/transformer.ts | 2 - yarn.lock | 40 +++++++++---------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index a4f7b869aef6f..ce43089105268 100644 --- a/package.json +++ b/package.json @@ -349,8 +349,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 71517bc10404d..ee65a1cf79148 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts index 805f8f192bdb1..5d140155f75e4 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts @@ -14,8 +14,6 @@ export function transformer({ response: ESResponse; }): AvgDurationByBrowserAPIResponse { const allUserAgentKeys = new Set( - // TODO(TS-3.7-ESLINT) - // eslint-disable-next-line @typescript-eslint/camelcase (response.aggregations?.user_agent_keys?.buckets ?? []).map(({ key }) => key.toString() ) diff --git a/yarn.lock b/yarn.lock index 7e965979fd46f..1cf41a3ecd57c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4154,24 +4154,24 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.8.0.tgz#eca584d46094ebebc3cb3e9fb625bfbc904a534d" - integrity sha512-ohqul5s6XEB0AzPWZCuJF5Fd6qC0b4+l5BGEnrlpmvXxvyymb8yw8Bs4YMF8usNAeuCJK87eFIHy8g8GFvOtGA== +"@typescript-eslint/eslint-plugin@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.9.0.tgz#fa810282c0e45f6c2310b9c0dfcd25bff97ab7e9" + integrity sha512-98rfOt3NYn5Gr9wekTB8TexxN6oM8ZRvYuphPs1Atfsy419SDLYCaE30aJkRiiTCwGEY98vOhFsEVm7Zs4toQQ== dependencies: - "@typescript-eslint/experimental-utils" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" - integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== +"@typescript-eslint/experimental-utils@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.9.0.tgz#bbe99a8d9510240c055fc4b17230dd0192ba3c7f" + integrity sha512-0lOLFdpdJsCMqMSZT7l7W2ta0+GX8A3iefG3FovJjrX+QR8y6htFlFdU7aOVPL6pDvt6XcsOb8fxk5sq+girTw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-scope "^5.0.0" "@typescript-eslint/experimental-utils@^1.13.0": @@ -4183,14 +4183,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" -"@typescript-eslint/parser@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.8.0.tgz#e10f7c40c8cf2fb19920c879311e6c46ad17bacb" - integrity sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw== +"@typescript-eslint/parser@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.9.0.tgz#2e9cf438de119b143f642a3a406e1e27eb70b7cd" + integrity sha512-fJ+dNs3CCvEsJK2/Vg5c2ZjuQ860ySOAsodDPwBaVlrGvRN+iCNC8kUfLFL8cT49W4GSiLPa/bHiMjYXA7EhKQ== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.8.0" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -4201,10 +4201,10 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/typescript-estree@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" - integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== +"@typescript-eslint/typescript-estree@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.9.0.tgz#09138daf8f47d0e494ba7db9e77394e928803017" + integrity sha512-v6btSPXEWCP594eZbM+JCXuFoXWXyF/z8kaSBSdCb83DF+Y7+xItW29SsKtSULgLemqJBT+LpT+0ZqdfH7QVmA== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" From 3b6e51b2d8078180a0e6e7cfdadead4d6ae07ead Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 26 Nov 2019 19:56:52 +0100 Subject: [PATCH 095/128] [SIEM] Fix styled-components bump in siem app (#51559) --- .../components/edit_data_provider/index.tsx | 176 +++++++++--------- .../siem/public/components/loading/index.tsx | 46 ++--- .../siem/public/components/page/index.tsx | 16 +- .../column_headers/events_select/index.tsx | 4 +- .../timeline/data_providers/empty.tsx | 2 +- .../timeline/data_providers/providers.tsx | 2 +- .../components/timeline/properties/index.tsx | 39 ---- .../search_or_filter/search_or_filter.tsx | 88 ++++----- .../public/components/wrapper_page/index.tsx | 2 + 9 files changed, 177 insertions(+), 198 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx index 214ac926e8868..18b271a3abc29 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx @@ -49,8 +49,7 @@ HeaderContainer.displayName = 'HeaderContainer'; // SIDE EFFECT: the following `createGlobalStyle` overrides the default styling // of euiComboBoxOptionsList because it's implemented as a popover, so it's // not selectable as a child of the styled component -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const StatefulEditDataProviderGlobalStyle = createGlobalStyle` .euiComboBoxOptionsList { z-index: 9999; } @@ -158,104 +157,107 @@ export const StatefulEditDataProvider = React.memo( }, []); return ( - - - - - - - 0 ? updatedField[0].label : null}> + <> + + + + + + + 0 ? updatedField[0].label : null}> + + + + + + + - - - + + + + + + + + + {updatedOperator.length > 0 && + updatedOperator[0].label !== i18n.EXISTS && + updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - + - - - - - - + ) : null} - {updatedOperator.length > 0 && - updatedOperator[0].label !== i18n.EXISTS && - updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - - + - ) : null} - - - - - - - - { - onDataProviderEdited({ - andProviderId, - excluded: getExcludedFromSelection(updatedOperator), - field: updatedField.length > 0 ? updatedField[0].label : '', - id: timelineId, - operator: getQueryOperatorFromSelection(updatedOperator), - providerId, - value: updatedValue, - }); - }} - size="s" - > - {i18n.SAVE} - - - - - - + + + + { + onDataProviderEdited({ + andProviderId, + excluded: getExcludedFromSelection(updatedOperator), + field: updatedField.length > 0 ? updatedField[0].label : '', + id: timelineId, + operator: getQueryOperatorFromSelection(updatedOperator), + providerId, + value: updatedValue, + }); + }} + size="s" + > + {i18n.SAVE} + + + + + + + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx index eb85edce78a8f..42867c09b971b 100644 --- a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx @@ -10,8 +10,7 @@ import { pure } from 'recompose'; import styled, { createGlobalStyle } from 'styled-components'; // SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const LoadingPanelGlobalStyle = createGlobalStyle` .euiPanel-loading-hide-border { border: none; } @@ -41,27 +40,30 @@ export const LoadingPanel = pure( position = 'relative', zIndex = 'inherit', }) => ( - - - - - - - + <> + + + + + + + - - {text} - - - - - + + {text} + + + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index bc701006c3a9c..d56012de88929 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -15,9 +15,12 @@ import { } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; -// SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +/* + SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly + and `EuiPopover`, `EuiToolTip` global styles +*/ + +export const AppGlobalStyle = createGlobalStyle` div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -25,6 +28,13 @@ createGlobalStyle` div.application { background-color: rgba(0,0,0,0); } + + .euiPopover__panel.euiPopover__panel-isOpen { + z-index: 9900 !important; + } + .euiToolTip { + z-index: 9950 !important; + } `; export const DescriptionListStyled = styled(EuiDescriptionList)` diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx index 747ef8f3ffe47..4f414af74a914 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx @@ -17,8 +17,7 @@ export const EVENTS_SELECT_WIDTH = 60; // px // SIDE EFFECT: the following `createGlobalStyle` overrides // the style of the select items -// eslint-disable-next-line -createGlobalStyle` +const EventsSelectGlobalStyle = createGlobalStyle` .eventsSelectItem { width: 100% !important; @@ -73,6 +72,7 @@ export const EventsSelect = pure(({ checkState, timelineId }) => { /> +
    ); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index 29d2df5172457..3ef7240ee0375 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -50,7 +50,7 @@ const HighlightedBackground = styled.span` HighlightedBackground.displayName = 'HighlightedBackground'; const EmptyContainer = styled.div<{ showSmallMsg: boolean }>` - width: ${props => (props.showSmallMsg ? '60px' : 'auto')} + width: ${props => (props.showSmallMsg ? '60px' : 'auto')}; align-items: center; display: flex; flex-direction: row; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index 112962367cd36..5a8654509fa88 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -46,7 +46,7 @@ interface Props { const ROW_OF_DATA_PROVIDERS_HEIGHT = 43; // px const PanelProviders = styled.div` - position: relative + position: relative; display: flex; flex-direction: row; min-height: 100px; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index ccc222673d7bc..7b69e006f48ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAvatar, EuiFlexItem, EuiIcon } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; -import styled, { createGlobalStyle } from 'styled-components'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -22,43 +20,6 @@ type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; -// SIDE EFFECT: the following `createGlobalStyle` overrides `EuiPopover` -// and `EuiToolTip` global styles: -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` - .euiPopover__panel.euiPopover__panel-isOpen { - z-index: 9900 !important; - } - .euiToolTip { - z-index: 9950 !important; - } -`; - -const Avatar = styled(EuiAvatar)` - margin-left: 5px; -`; - -Avatar.displayName = 'Avatar'; - -const DescriptionPopoverMenuContainer = styled.div` - margin-top: 15px; -`; - -DescriptionPopoverMenuContainer.displayName = 'DescriptionPopoverMenuContainer'; - -const SettingsIcon = styled(EuiIcon)` - margin-left: 4px; - cursor: pointer; -`; - -SettingsIcon.displayName = 'SettingsIcon'; - -const HiddenFlexItem = styled(EuiFlexItem)` - display: none; -`; - -HiddenFlexItem.displayName = 'HiddenFlexItem'; - interface Props { associateNote: AssociateNote; createTimeline: CreateTimeline; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 2d953ce3cfc95..eaa476bf3e2b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -26,8 +26,7 @@ const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; const searchOrFilterPopoverWidth = '352px'; // SIDE EFFECT: the following creates a global class selector -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const SearchOrFilterGlobalStyle = createGlobalStyle` .${timelineSelectModeItemsClassName} { width: 350px !important; } @@ -110,48 +109,51 @@ export const SearchOrFilter = pure( updateKqlMode, updateReduxTime, }) => ( - - - - - updateKqlMode({ id: timelineId, kqlMode: mode })} - options={options} - popoverClassName={searchOrFilterPopoverClassName} - valueOfSelected={kqlMode} + <> + + + + + updateKqlMode({ id: timelineId, kqlMode: mode })} + options={options} + popoverClassName={searchOrFilterPopoverClassName} + valueOfSelected={kqlMode} + /> + + + + - - - - - - - + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx index 5998aa527206e..309693427459e 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import styled, { css } from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; +import { AppGlobalStyle } from '../page/index'; const Wrapper = styled.div` ${({ theme }) => css` @@ -54,6 +55,7 @@ export const WrapperPage = React.memo( return ( {children} + ); } From f5296293c25504f280ce18c70f90f9e5e53e95c3 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 26 Nov 2019 14:22:02 -0500 Subject: [PATCH 096/128] [Canvas] Move workpad api routes to New Platform (#51116) * Move workpad api routes to New Platform * Cleanup * Clean up/Pr Feedback * Adding missing dependency to tests * Fix typecheck * Loosen workpad schema restrictions --- .../canvas/__tests__/fixtures/workpads.ts | 13 + .../canvas/public/lib/workpad_service.js | 1 + .../plugins/canvas/server/routes/index.ts | 2 - .../canvas/server/routes/workpad.test.js | 462 ------------------ .../plugins/canvas/server/routes/workpad.ts | 254 ---------- x-pack/plugins/canvas/kibana.json | 10 + x-pack/plugins/canvas/server/index.ts | 11 + x-pack/plugins/canvas/server/plugin.ts | 25 + .../server/routes/catch_error_handler.ts | 30 ++ x-pack/plugins/canvas/server/routes/index.ts | 17 + .../server/routes/workpad/create.test.ts | 102 ++++ .../canvas/server/routes/workpad/create.ts | 57 +++ .../server/routes/workpad/delete.test.ts | 78 +++ .../canvas/server/routes/workpad/delete.ts | 32 ++ .../canvas/server/routes/workpad/find.test.ts | 113 +++++ .../canvas/server/routes/workpad/find.ts | 60 +++ .../canvas/server/routes/workpad/get.test.ts | 140 ++++++ .../canvas/server/routes/workpad/get.ts | 65 +++ .../canvas/server/routes/workpad/index.ts | 21 + .../server/routes/workpad/ok_response.ts | 9 + .../server/routes/workpad/update.test.ts | 223 +++++++++ .../canvas/server/routes/workpad/update.ts | 129 +++++ .../server/routes/workpad/workpad_schema.ts | 65 +++ 23 files changed, 1201 insertions(+), 718 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/workpad.test.js delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/workpad.ts create mode 100644 x-pack/plugins/canvas/kibana.json create mode 100644 x-pack/plugins/canvas/server/index.ts create mode 100644 x-pack/plugins/canvas/server/plugin.ts create mode 100644 x-pack/plugins/canvas/server/routes/catch_error_handler.ts create mode 100644 x-pack/plugins/canvas/server/routes/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/create.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/create.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/delete.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/delete.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/find.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/find.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/get.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/get.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/ok_response.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/update.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/update.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts index d7ebbd87c97e6..271fc7a979057 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts @@ -192,3 +192,16 @@ export const elements: CanvasElement[] = [ { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, { ...BaseElement, expression: 'image | render' }, ]; + +export const workpadWithGroupAsElement: CanvasWorkpad = { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'image | render' }, + { ...BaseElement, id: 'group-1234' }, + ], + }, + ], +}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js index 33067f1837f41..f1ed069c15d4d 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -29,6 +29,7 @@ export function get(workpadId) { }); } +// TODO: I think this function is never used. Look into and remove the corresponding route as well export function update(id, workpad) { return fetch.put(`${apiPath}/${id}`, workpad); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts index a0502c5e891a2..515d5b5e895ed 100644 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ b/x-pack/legacy/plugins/canvas/server/routes/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { workpad } from './workpad'; import { esFields } from './es_fields'; import { customElements } from './custom_elements'; import { shareableWorkpads } from './shareables'; @@ -13,6 +12,5 @@ import { CoreSetup } from '../shim'; export function routes(setup: CoreSetup): void { customElements(setup.http.route, setup.elasticsearch); esFields(setup.http.route, setup.elasticsearch); - workpad(setup.http.route, setup.elasticsearch); shareableWorkpads(setup.http.route); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js b/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js deleted file mode 100644 index 09a5c3b89c31e..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js +++ /dev/null @@ -1,462 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { workpad } from './workpad'; - -const routePrefix = API_ROUTE_WORKPAD; -const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; -const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - -jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); - -describe(`${CANVAS_TYPE} API`, () => { - const savedObjectsClient = { - get: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - }; - - afterEach(() => { - savedObjectsClient.get.mockReset(); - savedObjectsClient.create.mockReset(); - savedObjectsClient.delete.mockReset(); - savedObjectsClient.find.mockReset(); - }); - - // Mock toISOString function of all Date types - global.Date = class Date extends global.Date { - toISOString() { - return '2019-02-12T21:01:22.479Z'; - } - }; - - // Setup mock server - const mockServer = new Hapi.Server({ debug: false, port: 0 }); - const mockEs = { - getCluster: () => ({ - errors: { - // formatResponse will fail without objects here - '400': Error, - '401': Error, - '403': Error, - '404': Error, - }, - }), - }; - - mockServer.ext('onRequest', (req, h) => { - req.getSavedObjectsClient = () => savedObjectsClient; - return h.continue; - }); - workpad(mockServer.route.bind(mockServer), mockEs); - - describe(`GET ${routePrefix}/{id}`, () => { - test('returns successful response', async () => { - const request = { - method: 'GET', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ id: '123', attributes: { foo: true } }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "foo": true, - "id": "123", -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - describe(`POST ${routePrefix}`, () => { - test('returns successful response without id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "workpad-123abc", - }, - ], -] -`); - }); - - test('returns succesful response with id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - id: '123', - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'PUT', - url: `${routePrefix}/123`, - payload: { - id: '234', - foo: true, - }, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`DELETE ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'DELETE', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.delete.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.delete.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - it(`GET ${routePrefix}/find`, async () => { - const request = { - method: 'GET', - url: `${routePrefix}/find?name=abc&page=2&perPage=10`, - }; - - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - attributes: { - foo: true, - }, - }, - ], - }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "workpads": Array [ - Object { - "foo": true, - "id": "1", - }, - ], -} -`); - expect(savedObjectsClient.find.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - Object { - "fields": Array [ - "id", - "name", - "@created", - "@timestamp", - ], - "page": "2", - "perPage": "10", - "search": "abc* | abc", - "searchFields": Array [ - "name", - ], - "sortField": "@timestamp", - "sortOrder": "desc", - "type": "canvas-workpad", - }, - ], -] -`); - }); - - describe(`PUT ${routePrefixAssets}/{id}`, () => { - test('only updates assets', async () => { - const request = { - method: 'PUT', - url: `${routePrefixAssets}/123`, - payload: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - 'asset-456': { - id: 'asset-456', - '@created': '2019-02-15T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }; - - // provide some existing workpad data to check that it's preserved - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - "asset-456": Object { - "@created": "2019-02-15T00:00:00.000Z", - "id": "asset-456", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "name": "fake workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefixStructures}/{id}`, () => { - test('only updates workpad', async () => { - const request = { - method: 'PUT', - url: `${routePrefixStructures}/123`, - payload: { - name: 'renamed workpad', - css: '.canvasPage { color: LavenderBlush; }', - }, - }; - - // provide some existing asset data and a name to replace - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - assets: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "css": ".canvasPage { color: LavenderBlush; }", - "name": "renamed workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts b/x-pack/legacy/plugins/canvas/server/routes/workpad.ts deleted file mode 100644 index 380fe97ca9ef1..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import boom from 'boom'; -import { omit } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { getId } from '../../public/lib/get_id'; -import { CoreSetup } from '../shim'; -// @ts-ignore Untyped Local -import { formatResponse as formatRes } from '../lib/format_response'; -import { CanvasWorkpad } from '../../types'; - -type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - -interface WorkpadRequestFacade { - getSavedObjectsClient: () => SavedObjectsClientContract; -} - -type WorkpadRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad; -}; - -type FindWorkpadRequest = WorkpadRequestFacade & { - query: { - name: string; - page: number; - perPage: number; - }; -}; - -type AssetsRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad['assets']; -}; - -export function workpad( - route: CoreSetup['http']['route'], - elasticsearch: CoreSetup['elasticsearch'] -) { - // @ts-ignore EsErrors is not on the Cluster type - const { errors: esErrors } = elasticsearch.getCluster('data'); - const routePrefix = API_ROUTE_WORKPAD; - const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; - const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - const formatResponse = formatRes(esErrors); - - function createWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - - if (!req.payload) { - return Promise.reject(boom.badRequest('A workpad payload is required')); - } - - const now = new Date().toISOString(); - const { id, ...payload } = req.payload; - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id: id || getId('workpad') } - ); - } - - function updateWorkpad( - req: WorkpadRequest | AssetsRequest, - newPayload?: CanvasWorkpad | { assets: CanvasWorkpad['assets'] } - ) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - const payload = newPayload ? newPayload : req.payload; - - const now = new Date().toISOString(); - - return savedObjectsClient.get(CANVAS_TYPE, id).then(workpadObject => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...(workpadObject.attributes as SavedObjectAttributes), - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': workpadObject.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - }); - } - - function deleteWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient.delete(CANVAS_TYPE, id); - } - - function findWorkpad(req: FindWorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { name, page, perPage } = req.query; - - return savedObjectsClient.find({ - type: CANVAS_TYPE, - sortField: '@timestamp', - sortOrder: 'desc', - search: name ? `${name}* | ${name}` : '*', - searchFields: ['name'], - fields: ['id', 'name', '@created', '@timestamp'], - page, - perPage, - }); - } - - // get workpad - route({ - method: 'GET', - path: `${routePrefix}/{id}`, - handler(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient - .get(CANVAS_TYPE, id) - .then(obj => { - if ( - // not sure if we need to be this defensive - obj.type === 'canvas-workpad' && - obj.attributes && - obj.attributes.pages && - obj.attributes.pages.length - ) { - obj.attributes.pages.forEach(page => { - const elements = (page.elements || []).filter( - ({ id: pageId }) => !pageId.startsWith('group') - ); - const groups = (page.groups || []).concat( - (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) - ); - page.elements = elements; - page.groups = groups; - }); - } - return obj; - }) - .then(obj => ({ id: obj.id, ...obj.attributes })) - .then(formatResponse) - .catch(formatResponse); - }, - }); - - // create workpad - route({ - method: 'POST', - path: routePrefix, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return createWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad - route({ - method: 'PUT', - path: `${routePrefix}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad assets - route({ - method: 'PUT', - path: `${routePrefixAssets}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: AssetsRequest) { - const payload = { assets: request.payload }; - return updateWorkpad(request, payload) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad structures - route({ - method: 'PUT', - path: `${routePrefixStructures}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // delete workpad - route({ - method: 'DELETE', - path: `${routePrefix}/{id}`, - handler(request: WorkpadRequest) { - return deleteWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // find workpads - route({ - method: 'GET', - path: `${routePrefix}/find`, - handler(request: FindWorkpadRequest) { - return findWorkpad(request) - .then(formatResponse) - .then(resp => { - return { - total: resp.total, - workpads: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), - }; - }) - .catch(() => { - return { - total: 0, - workpads: [], - }; - }); - }, - }); -} diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json new file mode 100644 index 0000000000000..87214f0287054 --- /dev/null +++ b/x-pack/plugins/canvas/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "canvas", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "canvas"], + "server": true, + "ui": false, + "requiredPlugins": [] + } + \ No newline at end of file diff --git a/x-pack/plugins/canvas/server/index.ts b/x-pack/plugins/canvas/server/index.ts new file mode 100644 index 0000000000000..e881f7db69c78 --- /dev/null +++ b/x-pack/plugins/canvas/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/server'; +import { CanvasPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new CanvasPlugin(initializerContext); diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts new file mode 100644 index 0000000000000..76b86c2ac39b4 --- /dev/null +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, PluginInitializerContext, Plugin, Logger } from 'src/core/server'; +import { initRoutes } from './routes'; + +export class CanvasPlugin implements Plugin { + private readonly logger: Logger; + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(coreSetup: CoreSetup): void { + const canvasRouter = coreSetup.http.createRouter(); + + initRoutes({ router: canvasRouter, logger: this.logger }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts new file mode 100644 index 0000000000000..fb7f4d6ee2600 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ObjectType } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; + +export const catchErrorHandler: < + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType +>( + fn: RequestHandler +) => RequestHandler = fn => { + return async (context, request, response) => { + try { + return await fn(context, request, response); + } catch (error) { + if (error.isBoom) { + return response.customError({ + body: error.output.payload, + statusCode: error.output.statusCode, + }); + } + return response.internalError({ body: error }); + } + }; +}; diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts new file mode 100644 index 0000000000000..46873a6b32542 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, Logger } from 'src/core/server'; +import { initWorkpadRoutes } from './workpad'; + +export interface RouteInitializerDeps { + router: IRouter; + logger: Logger; +} + +export function initRoutes(deps: RouteInitializerDeps) { + initWorkpadRoutes(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts new file mode 100644 index 0000000000000..dbad1a97dc458 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeCreateWorkpadRoute } from './create'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const mockedUUID = '123abc'; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('POST workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + + const router = httpService.createRouter('') as jest.Mocked; + initializeCreateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it(`returns 200 when the workpad is created`, async () => { + const mockWorkpad = { + pages: [], + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: mockWorkpad, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...mockWorkpad, + '@timestamp': nowIso, + '@created': nowIso, + }, + { + id: `workpad-${mockedUUID}`, + } + ); + }); + + it(`returns bad request if create is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts new file mode 100644 index 0000000000000..be904356720b6 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { WorkpadSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_WORKPAD}`, + validate: { + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + if (!request.body) { + return response.badRequest({ body: 'A workpad payload is required' }); + } + + const workpad = request.body as CanvasWorkpad; + + const now = new Date().toISOString(); + const { id, ...payload } = workpad; + + await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + ...payload, + '@timestamp': now, + '@created': now, + }, + { id: id || getId('workpad') } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts new file mode 100644 index 0000000000000..e693840826b7a --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeDeleteWorkpadRoute } from './delete'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('DELETE workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDeleteWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it(`returns 200 ok when the workpad is deleted`, async () => { + const id = 'some-id'; + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.delete).toBeCalledWith(CANVAS_TYPE, id); + }); + + it(`returns bad request if delete is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/some-id`, + params: { + id: 'some-id', + }, + }); + + (mockRouteContext.core.savedObjects.client.delete as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.ts new file mode 100644 index 0000000000000..7adf11e7a887b --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeDeleteWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.delete( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + context.core.savedObjects.client.delete(CANVAS_TYPE, request.params.id); + return response.ok({ body: okResponse }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts new file mode 100644 index 0000000000000..08de9b20e9818 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { initializeFindWorkpadsRoute } from './find'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('Find workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeFindWorkpadsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 with the found workpads`, async () => { + const name = 'something'; + const perPage = 10000; + const mockResults = { + total: 2, + saved_objects: [ + { id: 1, attributes: { key: 'value' } }, + { id: 2, attributes: { key: 'other-value' } }, + ], + }; + + const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock; + + findMock.mockResolvedValueOnce(mockResults); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name, + perPage, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(findMock.mock.calls[0][0].search).toBe(`${name}* | ${name}`); + expect(findMock.mock.calls[0][0].perPage).toBe(perPage); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 2, + "workpads": Array [ + Object { + "id": 1, + "key": "value", + }, + Object { + "id": 2, + "key": "other-value", + }, + ], + } + `); + }); + + it(`returns 200 with empty results on error`, async () => { + (mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => { + throw new Error('generic error'); + }); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name: 'something', + perPage: 1000, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 0, + "workpads": Array [], + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.ts b/x-pack/plugins/canvas/server/routes/workpad/find.ts new file mode 100644 index 0000000000000..a528a75611609 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { SavedObjectAttributes } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeFindWorkpadsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/find`, + validate: { + query: schema.object({ + name: schema.string(), + page: schema.maybe(schema.number()), + perPage: schema.number(), + }), + }, + }, + async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const { name, page, perPage } = request.query; + + try { + const workpads = await savedObjectsClient.find({ + type: CANVAS_TYPE, + sortField: '@timestamp', + sortOrder: 'desc', + search: name ? `${name}* | ${name}` : '*', + searchFields: ['name'], + fields: ['id', 'name', '@created', '@timestamp'], + page, + perPage, + }); + + return response.ok({ + body: { + total: workpads.total, + workpads: workpads.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), + }, + }); + } catch (error) { + return response.ok({ + body: { + total: 0, + workpads: [], + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts new file mode 100644 index 0000000000000..a31293f572c75 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeGetWorkpadRoute } from './get'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpadWithGroupAsElement } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('GET workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeGetWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 when the workpad is found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: { foo: true }, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "foo": true, + "id": "123", + } + `); + + expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "canvas-workpad", + "123", + ], + ] + `); + }); + + it('corrects elements that should be groups', async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: workpadWithGroupAsElement as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + const workpad = response.payload as CanvasWorkpad; + + expect(response.status).toBe(200); + expect(workpad).not.toBeUndefined(); + + expect(workpad.pages[0].elements.length).toBe(1); + expect(workpad.pages[0].groups.length).toBe(1); + }); + + it('returns 404 if the workpad is not found', async () => { + const id = '123'; + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id, + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockImplementation(() => { + throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id); + }); + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "error": "Not Found", + "message": "Saved object [canvas-workpad/123] not found", + "statusCode": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts new file mode 100644 index 0000000000000..7a51006aa9f02 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + const workpad = await context.core.savedObjects.client.get( + CANVAS_TYPE, + request.params.id + ); + + if ( + // not sure if we need to be this defensive + workpad.type === 'canvas-workpad' && + workpad.attributes && + workpad.attributes.pages && + workpad.attributes.pages.length + ) { + workpad.attributes.pages.forEach(page => { + const elements = (page.elements || []).filter( + ({ id: pageId }) => !pageId.startsWith('group') + ); + const groups = (page.groups || []).concat( + (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) + ); + page.elements = elements; + page.groups = groups; + }); + } + + return response.ok({ + body: { + id: workpad.id, + ...workpad.attributes, + }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts new file mode 100644 index 0000000000000..8a61b30be5414 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { initializeFindWorkpadsRoute } from './find'; +import { initializeGetWorkpadRoute } from './get'; +import { initializeCreateWorkpadRoute } from './create'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { initializeDeleteWorkpadRoute } from './delete'; + +export function initWorkpadRoutes(deps: RouteInitializerDeps) { + initializeFindWorkpadsRoute(deps); + initializeGetWorkpadRoute(deps); + initializeCreateWorkpadRoute(deps); + initializeUpdateWorkpadRoute(deps); + initializeUpdateWorkpadAssetsRoute(deps); + initializeDeleteWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts new file mode 100644 index 0000000000000..43d545a5183fe --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const okResponse = { + ok: true, +}; diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts new file mode 100644 index 0000000000000..492a6c98d71ee --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpads } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { okResponse } from './ok_response'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const workpad = workpads[0]; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('PUT workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + jest.resetAllMocks(); + clock.restore(); + }); + + it(`returns 200 ok when the workpad is updated`, async () => { + const updatedWorkpad = { name: 'new name' }; + const { id, ...workpadAttributes } = workpad; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + body: updatedWorkpad, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: workpadAttributes as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(okResponse); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...workpadAttributes, + ...updatedWorkpad, + '@timestamp': nowIso, + '@created': workpad['@created'], + }, + { + overwrite: true, + id, + } + ); + }); + + it(`returns not found if existing workpad is not found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'not-found', + }, + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( + 'not found' + ); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(404); + }); + + it(`returns bad request if the write fails`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'some-id', + }, + body: {}, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'some-id', + type: CANVAS_TYPE, + attributes: {}, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); + +describe('update assets', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadAssetsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it('updates assets', async () => { + const { id, ...attributes } = workpad; + const assets = { + 'asset-1': { + '@created': new Date().toISOString(), + id: 'asset-1', + type: 'asset', + value: 'some-url-encoded-asset', + }, + 'asset-2': { + '@created': new Date().toISOString(), + id: 'asset-2', + type: 'asset', + value: 'some-other asset', + }, + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad-assets/some-id', + params: { + id, + }, + body: assets, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: attributes as any, + references: [], + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...attributes, + '@timestamp': nowIso, + assets, + }, + { + id, + overwrite: true, + } + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts new file mode 100644 index 0000000000000..460aa174038ae --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { omit } from 'lodash'; +import { KibanaResponseFactory } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, + API_ROUTE_WORKPAD_STRUCTURES, + API_ROUTE_WORKPAD_ASSETS, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); + +const AssetPayloadSchema = schema.object({ + assets: AssetsRecordSchema, +}); + +const workpadUpdateHandler = async ( + payload: TypeOf | TypeOf, + id: string, + savedObjectsClient: SavedObjectsClientContract, + response: KibanaResponseFactory +) => { + const now = new Date().toISOString(); + + const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); + await savedObjectsClient.create( + CANVAS_TYPE, + { + ...workpadObject.attributes, + ...omit(payload, 'id'), // never write the id property + '@timestamp': now, // always update the modified time + '@created': workpadObject.attributes['@created'], // ensure created is not modified + }, + { overwrite: true, id } + ); + + return response.ok({ + body: okResponse, + }); +}; + +export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + // TODO: This route is likely deprecated and everything is using the workpad_structures + // path instead. Investigate further. + router.put( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); + + router.put( + { + path: `${API_ROUTE_WORKPAD_STRUCTURES}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); +} + +export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + + router.put( + { + path: `${API_ROUTE_WORKPAD_ASSETS}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + // ToDo: Currently the validation must be a schema.object + // Because we don't know what keys the assets will have, we have to allow + // unknowns and then validate in the handler + body: schema.object({}, { allowUnknowns: true }), + }, + }, + async (context, request, response) => { + return workpadUpdateHandler( + { assets: AssetsRecordSchema.validate(request.body) }, + request.params.id, + context.core.savedObjects.client, + response + ); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts new file mode 100644 index 0000000000000..0bcb161575901 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const WorkpadElementSchema = schema.object({ + expression: schema.string(), + filter: schema.maybe(schema.nullable(schema.string())), + id: schema.string(), + position: PositionSchema, +}); + +export const WorkpadPageSchema = schema.object({ + elements: schema.arrayOf(WorkpadElementSchema), + groups: schema.arrayOf( + schema.object({ + id: schema.string(), + position: PositionSchema, + }) + ), + id: schema.string(), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const WorkpadAssetSchema = schema.object({ + '@created': schema.string(), + id: schema.string(), + type: schema.string(), + value: schema.string(), +}); + +export const WorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), +}); From c41531122183749159a06641acef22ad5a230019 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 26 Nov 2019 23:28:04 +0300 Subject: [PATCH 097/128] Move @kbn/es-query into data plugin (#51014) --- .eslintignore | 2 +- .i18nrc.json | 1 - ...a-plugin-public.savedobjectsclient.find.md | 2 +- ...kibana-plugin-public.savedobjectsclient.md | 2 +- package.json | 1 - packages/kbn-es-query/README.md | 92 ---- packages/kbn-es-query/babel.config.js | 35 -- packages/kbn-es-query/index.d.ts | 20 - packages/kbn-es-query/package.json | 28 -- packages/kbn-es-query/scripts/build.js | 20 - .../src/__fixtures__/filter_skeleton.json | 5 - .../__fixtures__/index_pattern_response.json | 303 ------------- packages/kbn-es-query/src/index.d.ts | 20 - packages/kbn-es-query/src/index.js | 20 - .../src/kuery/ast/__tests__/ast.js | 415 ----------------- packages/kbn-es-query/src/kuery/ast/ast.d.ts | 50 --- .../kbn-es-query/src/kuery/ast/index.d.ts | 20 - .../functions/__tests__/geo_bounding_box.js | 120 ----- .../kuery/functions/__tests__/geo_polygon.js | 131 ------ .../src/kuery/functions/__tests__/is.js | 310 ------------- .../utils/get_full_field_name_node.js | 88 ---- .../kuery/node_types/__tests__/function.js | 80 ---- .../kuery/node_types/__tests__/named_arg.js | 62 --- .../kuery/node_types/__tests__/wildcard.js | 107 ----- .../__tests__/get_time_zone_from_settings.js | 36 -- packages/kbn-es-query/src/utils/filters.js | 133 ------ .../src/utils/get_time_zone_from_settings.js | 28 -- packages/kbn-es-query/src/utils/index.js | 20 - packages/kbn-es-query/tasks/build_cli.js | 103 ----- packages/kbn-es-query/tsconfig.browser.json | 11 - packages/kbn-es-query/tsconfig.json | 11 - src/core/public/public.api.md | 2 +- .../service/lib/filter_utils.test.ts | 22 +- .../saved_objects/service/lib/filter_utils.ts | 76 ++-- .../service/lib/repository.test.js | 3 +- .../saved_objects/service/lib/repository.ts | 8 +- .../service/lib/search_dsl/query_params.ts | 6 +- .../service/lib/search_dsl/search_dsl.ts | 4 +- .../components/query_bar_top_row.tsx | 7 +- .../public/vis/timelion_request_handler.ts | 2 +- .../public/components/vis_editor.js | 4 +- .../public/vega_request_handler.ts | 2 +- .../es_query/es_query/build_es_query.test.ts | 2 +- .../es_query/es_query/build_es_query.ts | 2 +- .../es_query/filter_matches_index.test.ts | 3 +- .../es_query/es_query/filter_matches_index.ts | 2 +- .../common/es_query/es_query/from_filters.ts | 2 +- .../es_query/es_query/from_kuery.test.ts | 4 +- .../common/es_query/es_query/from_kuery.ts | 30 +- .../es_query/es_query/migrate_filter.test.ts | 6 +- .../es_query/es_query/migrate_filter.ts | 2 +- src/plugins/data/common/es_query/index.ts | 3 +- .../es_query/kuery/ast/_generated_}/kuery.js | 0 .../common/es_query/kuery/ast/ast.test.ts | 421 ++++++++++++++++++ .../data/common/es_query/kuery/ast/ast.ts | 89 ++-- .../data/common/es_query/kuery/ast/index.ts | 0 .../data/common/es_query}/kuery/ast/kuery.peg | 0 .../common/es_query}/kuery/functions/and.js | 0 .../es_query/kuery/functions/and.test.ts | 54 ++- .../es_query}/kuery/functions/exists.js | 0 .../es_query/kuery/functions/exists.test.ts | 73 +-- .../kuery/functions/geo_bounding_box.js | 0 .../kuery/functions/geo_bounding_box.test.ts | 133 ++++++ .../es_query}/kuery/functions/geo_polygon.js | 0 .../kuery/functions/geo_polygon.test.ts | 143 ++++++ .../common/es_query}/kuery/functions/index.js | 0 .../common/es_query}/kuery/functions/is.js | 20 +- .../es_query/kuery/functions/is.test.ts | 305 +++++++++++++ .../es_query}/kuery/functions/nested.js | 0 .../es_query/kuery/functions/nested.test.ts | 56 ++- .../common/es_query}/kuery/functions/not.js | 0 .../es_query/kuery/functions/not.test.ts | 52 ++- .../common/es_query}/kuery/functions/or.js | 0 .../es_query/kuery/functions/or.test.ts | 63 +-- .../common/es_query}/kuery/functions/range.js | 2 +- .../es_query/kuery/functions/range.test.ts | 206 +++++---- .../kuery/functions/utils/get_fields.js | 0 .../kuery/functions/utils/get_fields.test.ts | 73 ++- .../utils/get_full_field_name_node.js | 0 .../utils/get_full_field_name_node.test.ts | 87 ++++ .../data/common/es_query/kuery/index.ts | 6 +- .../es_query/kuery/kuery_syntax_error.test.ts | 71 +-- .../es_query/kuery/kuery_syntax_error.ts | 57 ++- .../es_query}/kuery/node_types/function.js | 0 .../kuery/node_types/function.test.ts | 75 ++++ .../es_query}/kuery/node_types/index.d.ts | 44 +- .../es_query}/kuery/node_types/index.js | 0 .../es_query}/kuery/node_types/literal.js | 0 .../es_query/kuery/node_types/literal.test.ts | 35 +- .../es_query}/kuery/node_types/named_arg.js | 0 .../kuery/node_types/named_arg.test.ts | 57 +++ .../es_query}/kuery/node_types/wildcard.js | 0 .../kuery/node_types/wildcard.test.ts | 110 +++++ .../data/common/es_query/kuery/types.ts | 23 +- .../common/field_formats/converters/custom.ts | 4 +- .../data/common/field_formats/field_format.ts | 2 +- tasks/config/peg.js | 4 +- .../components/shared/KueryBar/index.tsx | 12 +- .../convert_ui_filters/get_ui_filters_es.ts | 14 +- .../public/lib/adapters/elasticsearch/rest.ts | 9 +- .../graph/public/components/search_bar.tsx | 9 +- .../components/metrics_explorer/kuery_bar.tsx | 4 +- .../store/local/log_filter/selectors.ts | 6 +- .../store/local/waffle_filter/selectors.ts | 5 +- .../plugins/infra/public/utils/kuery.ts | 6 +- .../public/autocomplete_providers/index.js | 4 +- .../components/kql_filter_bar/utils.js | 8 +- .../plugins/siem/public/lib/keury/index.ts | 12 +- .../components/step_define_rule/schema.tsx | 4 +- .../transform/public/app/lib/kibana/common.ts | 2 +- .../components/functional/kuery_bar/index.tsx | 17 +- .../plugins/uptime/public/pages/overview.tsx | 7 +- x-pack/package.json | 1 - .../translations/translations/ja-JP.json | 12 +- .../translations/translations/zh-CN.json | 12 +- 115 files changed, 2025 insertions(+), 2852 deletions(-) delete mode 100644 packages/kbn-es-query/README.md delete mode 100644 packages/kbn-es-query/babel.config.js delete mode 100644 packages/kbn-es-query/index.d.ts delete mode 100644 packages/kbn-es-query/package.json delete mode 100644 packages/kbn-es-query/scripts/build.js delete mode 100644 packages/kbn-es-query/src/__fixtures__/filter_skeleton.json delete mode 100644 packages/kbn-es-query/src/__fixtures__/index_pattern_response.json delete mode 100644 packages/kbn-es-query/src/index.d.ts delete mode 100644 packages/kbn-es-query/src/index.js delete mode 100644 packages/kbn-es-query/src/kuery/ast/__tests__/ast.js delete mode 100644 packages/kbn-es-query/src/kuery/ast/ast.d.ts delete mode 100644 packages/kbn-es-query/src/kuery/ast/index.d.ts delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/is.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/function.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js delete mode 100644 packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js delete mode 100644 packages/kbn-es-query/src/utils/filters.js delete mode 100644 packages/kbn-es-query/src/utils/get_time_zone_from_settings.js delete mode 100644 packages/kbn-es-query/src/utils/index.js delete mode 100644 packages/kbn-es-query/tasks/build_cli.js delete mode 100644 packages/kbn-es-query/tsconfig.browser.json delete mode 100644 packages/kbn-es-query/tsconfig.json rename {packages/kbn-es-query/src/kuery/ast => src/plugins/data/common/es_query/kuery/ast/_generated_}/kuery.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/ast/ast.test.ts rename packages/kbn-es-query/src/kuery/ast/ast.js => src/plugins/data/common/es_query/kuery/ast/ast.ts (53%) rename packages/kbn-es-query/src/kuery/ast/index.js => src/plugins/data/common/es_query/kuery/ast/index.ts (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/ast/kuery.peg (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/and.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/and.js => src/plugins/data/common/es_query/kuery/functions/and.test.ts (50%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/exists.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/exists.js => src/plugins/data/common/es_query/kuery/functions/exists.test.ts (51%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/geo_bounding_box.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/geo_polygon.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/index.js (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/is.js (95%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/is.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/nested.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/nested.js => src/plugins/data/common/es_query/kuery/functions/nested.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/not.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/not.js => src/plugins/data/common/es_query/kuery/functions/not.test.ts (50%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/or.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/or.js => src/plugins/data/common/es_query/kuery/functions/or.test.ts (52%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/range.js (98%) rename packages/kbn-es-query/src/kuery/functions/__tests__/range.js => src/plugins/data/common/es_query/kuery/functions/range.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/utils/get_fields.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js => src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts (52%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/utils/get_full_field_name_node.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts rename packages/kbn-es-query/src/kuery/index.js => src/plugins/data/common/es_query/kuery/index.ts (91%) rename packages/kbn-es-query/src/kuery/errors/index.test.js => src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts (66%) rename packages/kbn-es-query/src/kuery/errors/index.js => src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts (55%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/function.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/function.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/index.d.ts (72%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/index.js (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/literal.js (100%) rename packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js => src/plugins/data/common/es_query/kuery/node_types/literal.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/named_arg.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/wildcard.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts rename packages/kbn-es-query/src/kuery/index.d.ts => src/plugins/data/common/es_query/kuery/types.ts (73%) diff --git a/.eslintignore b/.eslintignore index cf13fc28467d9..90155ca9cb681 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,6 +8,7 @@ bower_components /plugins /built_assets /html_docs +/src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/fixtures/vislib/mock_data /src/legacy/ui/public/angular-bootstrap /src/legacy/ui/public/flot-charts @@ -19,7 +20,6 @@ bower_components /src/core/lib/kbn_internal_native_observable /packages/*/target /packages/eslint-config-kibana -/packages/kbn-es-query/src/kuery/ast/kuery.js /packages/kbn-pm/dist /packages/kbn-plugin-generator/sao_template/template /packages/kbn-ui-framework/dist diff --git a/.i18nrc.json b/.i18nrc.json index 2cdf7d2b039c6..e5ba6762da154 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -16,7 +16,6 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "kbnESQuery": "packages/kbn-es-query", "kbnVislibVisTypes": "src/legacy/core_plugins/kbn_vislib_vis_types", "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 866755e78648a..cecceb04240e6 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 50451b813a61c..c4ceb47f66e1b 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
    id: string;
    type: string;
    }[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/package.json b/package.json index ce43089105268..2c8d4ad4307b1 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,6 @@ "@kbn/babel-code-parser": "1.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", - "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", diff --git a/packages/kbn-es-query/README.md b/packages/kbn-es-query/README.md deleted file mode 100644 index fc403447877d8..0000000000000 --- a/packages/kbn-es-query/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# kbn-es-query - -This module is responsible for generating Elasticsearch queries for Kibana. See explanations below for each of the subdirectories. - -## es_query - -This folder contains the code that combines Lucene/KQL queries and filters into an Elasticsearch query. - -```javascript -buildEsQuery(indexPattern, queries, filters, config) -``` - -Generates the Elasticsearch query DSL from combining the queries and filters provided. - -```javascript -buildQueryFromFilters(filters, indexPattern) -``` - -Generates the Elasticsearch query DSL from the given filters. - -```javascript -luceneStringToDsl(query) -``` - -Generates the Elasticsearch query DSL from the given Lucene query. - -```javascript -migrateFilter(filter, indexPattern) -``` - -Migrates a filter from a previous version of Elasticsearch to the current version. - -```javascript -decorateQuery(query, queryStringOptions) -``` - -Decorates an Elasticsearch query_string query with the given options. - -## filters - -This folder contains the code related to Kibana Filter objects, including their definitions, and helper functions to create them. Filters in Kibana always contain a `meta` property which describes which `index` the filter corresponds to, as well as additional data about the specific filter. - -The object that is created by each of the following functions corresponds to a Filter object in the `lib` directory (e.g. `PhraseFilter`, `RangeFilter`, etc.) - -```javascript -buildExistsFilter(field, indexPattern) -``` - -Creates a filter (`ExistsFilter`) where the given field exists. - -```javascript -buildPhraseFilter(field, value, indexPattern) -``` - -Creates an filter (`PhraseFilter`) where the given field matches the given value. - -```javascript -buildPhrasesFilter(field, params, indexPattern) -``` - -Creates a filter (`PhrasesFilter`) where the given field matches one or more of the given values. `params` should be an array of values. - -```javascript -buildQueryFilter(query, index) -``` - -Creates a filter (`CustomFilter`) corresponding to a raw Elasticsearch query DSL object. - -```javascript -buildRangeFilter(field, params, indexPattern) -``` - -Creates a filter (`RangeFilter`) where the value for the given field is in the given range. `params` should contain `lt`, `lte`, `gt`, and/or `gte`. - -## kuery - -This folder contains the code corresponding to generating Elasticsearch queries using the Kibana query language. - -In general, you will only need to worry about the following functions from the `ast` folder: - -```javascript -fromExpression(expression) -``` - -Generates an abstract syntax tree corresponding to the raw Kibana query `expression`. - -```javascript -toElasticsearchQuery(node, indexPattern) -``` - -Takes an abstract syntax tree (generated from the previous method) and generates the Elasticsearch query DSL using the given `indexPattern`. Note that if no `indexPattern` is provided, then an Elasticsearch query DSL will still be generated, ignoring things like the index pattern scripted fields, field types, etc. - diff --git a/packages/kbn-es-query/babel.config.js b/packages/kbn-es-query/babel.config.js deleted file mode 100644 index 68783433fc711..0000000000000 --- a/packages/kbn-es-query/babel.config.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// We can't use common Kibana presets here because of babel versions incompatibility -module.exports = { - env: { - public: { - presets: [ - '@kbn/babel-preset/webpack_preset' - ], - }, - server: { - presets: [ - '@kbn/babel-preset/node_preset' - ], - }, - }, - ignore: ['**/__tests__/**/*', '**/*.test.ts', '**/*.test.tsx'], -}; diff --git a/packages/kbn-es-query/index.d.ts b/packages/kbn-es-query/index.d.ts deleted file mode 100644 index 9bbd0a193dfed..0000000000000 --- a/packages/kbn-es-query/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './src'; diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json deleted file mode 100644 index 2cd2a8f53d2ee..0000000000000 --- a/packages/kbn-es-query/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@kbn/es-query", - "main": "target/server/index.js", - "browser": "target/public/index.js", - "version": "1.0.0", - "license": "Apache-2.0", - "private": true, - "scripts": { - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --source-maps", - "kbn:watch": "node scripts/build --source-maps --watch" - }, - "dependencies": { - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", - "moment-timezone": "^0.5.27", - "@kbn/i18n": "1.0.0" - }, - "devDependencies": { - "@babel/cli": "^7.5.5", - "@babel/core": "^7.5.5", - "@kbn/babel-preset": "1.0.0", - "@kbn/dev-utils": "1.0.0", - "@kbn/expect": "1.0.0", - "del": "^5.1.0", - "getopts": "^2.2.4", - "supports-color": "^7.0.0" - } -} diff --git a/packages/kbn-es-query/scripts/build.js b/packages/kbn-es-query/scripts/build.js deleted file mode 100644 index 6d53a8469b0e0..0000000000000 --- a/packages/kbn-es-query/scripts/build.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -require('../tasks/build_cli'); diff --git a/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json b/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json deleted file mode 100644 index 1799d04a0fbd8..0000000000000 --- a/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "meta": { - "index": "logstash-*" - } -} diff --git a/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json b/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json deleted file mode 100644 index 588e6ada69cfe..0000000000000 --- a/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json +++ /dev/null @@ -1,303 +0,0 @@ -{ - "id": "logstash-*", - "title": "logstash-*", - "fields": [ - { - "name": "bytes", - "type": "number", - "esTypes": ["long"], - "count": 10, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "ssl", - "type": "boolean", - "esTypes": ["boolean"], - "count": 20, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@timestamp", - "type": "date", - "esTypes": ["date"], - "count": 30, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "time", - "type": "date", - "esTypes": ["date"], - "count": 30, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@tags", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "utc_time", - "type": "date", - "esTypes": ["date"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "phpmemory", - "type": "number", - "esTypes": ["integer"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "ip", - "type": "ip", - "esTypes": ["ip"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "request_body", - "type": "attachment", - "esTypes": ["attachment"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "point", - "type": "geo_point", - "esTypes": ["geo_point"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "area", - "type": "geo_shape", - "esTypes": ["geo_shape"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "hashed", - "type": "murmur3", - "esTypes": ["murmur3"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false - }, - { - "name": "geo.coordinates", - "type": "geo_point", - "esTypes": ["geo_point"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "extension", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "machine.os", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "machine.os.raw", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true, - "subType": { "multi": { "parent": "machine.os" } } - }, - { - "name": "geo.src", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "_id", - "type": "string", - "esTypes": ["_id"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "_type", - "type": "string", - "esTypes": ["_type"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "_source", - "type": "_source", - "esTypes": ["_source"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "non-filterable", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": false, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "non-sortable", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": false, - "aggregatable": false, - "readFromDocValues": false - }, - { - "name": "custom_user_field", - "type": "conflict", - "esTypes": ["long", "text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "script string", - "type": "string", - "count": 0, - "scripted": true, - "script": "'i am a string'", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script number", - "type": "number", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script date", - "type": "date", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "painless", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script murmur3", - "type": "murmur3", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "nestedField.child", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false, - "subType": { "nested": { "path": "nestedField" } } - }, - { - "name": "nestedField.nestedChild.doublyNestedChild", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false, - "subType": { "nested": { "path": "nestedField.nestedChild" } } - } - ] -} diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts deleted file mode 100644 index 79e6903b18644..0000000000000 --- a/packages/kbn-es-query/src/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './kuery'; diff --git a/packages/kbn-es-query/src/index.js b/packages/kbn-es-query/src/index.js deleted file mode 100644 index 79e6903b18644..0000000000000 --- a/packages/kbn-es-query/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './kuery'; diff --git a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js b/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js deleted file mode 100644 index 3cbe1203bc533..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as ast from '../ast'; -import expect from '@kbn/expect'; -import { nodeTypes } from '../../node_types/index'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery AST API', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('fromKueryExpression', function () { - - it('should return a match all "is" function for whitespace', function () { - const expected = nodeTypes.function.buildNode('is', '*', '*'); - const actual = ast.fromKueryExpression(' '); - expect(actual).to.eql(expected); - }); - - it('should return an "is" function with a null field for single literals', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo'); - const actual = ast.fromKueryExpression('foo'); - expect(actual).to.eql(expected); - }); - - it('should ignore extraneous whitespace at the beginning and end of the query', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo'); - const actual = ast.fromKueryExpression(' foo '); - expect(actual).to.eql(expected); - }); - - it('should not split on whitespace', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo bar'); - const actual = ast.fromKueryExpression('foo bar'); - expect(actual).to.eql(expected); - }); - - it('should support "and" as a binary operator', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]); - const actual = ast.fromKueryExpression('foo and bar'); - expect(actual).to.eql(expected); - }); - - it('should support "or" as a binary operator', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]); - const actual = ast.fromKueryExpression('foo or bar'); - expect(actual).to.eql(expected); - }); - - it('should support negation of queries with a "not" prefix', function () { - const expected = nodeTypes.function.buildNode('not', - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]) - ); - const actual = ast.fromKueryExpression('not (foo or bar)'); - expect(actual).to.eql(expected); - }); - - it('"and" should have a higher precedence than "or"', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', null, 'bar'), - nodeTypes.function.buildNode('is', null, 'baz'), - ]), - nodeTypes.function.buildNode('is', null, 'qux'), - ]) - ]); - const actual = ast.fromKueryExpression('foo or bar and baz or qux'); - expect(actual).to.eql(expected); - }); - - it('should support grouping to override default precedence', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]), - nodeTypes.function.buildNode('is', null, 'baz'), - ]); - const actual = ast.fromKueryExpression('(foo or bar) and baz'); - expect(actual).to.eql(expected); - }); - - it('should support matching against specific fields', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar'); - const actual = ast.fromKueryExpression('foo:bar'); - expect(actual).to.eql(expected); - }); - - it('should also not split on whitespace when matching specific fields', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz'); - const actual = ast.fromKueryExpression('foo:bar baz'); - expect(actual).to.eql(expected); - }); - - it('should treat quoted values as phrases', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz', true); - const actual = ast.fromKueryExpression('foo:"bar baz"'); - expect(actual).to.eql(expected); - }); - - it('should support a shorthand for matching multiple values against a single field', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'foo', 'bar'), - nodeTypes.function.buildNode('is', 'foo', 'baz'), - ]); - const actual = ast.fromKueryExpression('foo:(bar or baz)'); - expect(actual).to.eql(expected); - }); - - it('should support "and" and "not" operators and grouping in the shorthand as well', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'foo', 'bar'), - nodeTypes.function.buildNode('is', 'foo', 'baz'), - ]), - nodeTypes.function.buildNode('not', - nodeTypes.function.buildNode('is', 'foo', 'qux') - ), - ]); - const actual = ast.fromKueryExpression('foo:((bar or baz) and not qux)'); - expect(actual).to.eql(expected); - }); - - it('should support exclusive range operators', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('range', 'bytes', { - gt: 1000, - }), - nodeTypes.function.buildNode('range', 'bytes', { - lt: 8000, - }), - ]); - const actual = ast.fromKueryExpression('bytes > 1000 and bytes < 8000'); - expect(actual).to.eql(expected); - }); - - it('should support inclusive range operators', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('range', 'bytes', { - gte: 1000, - }), - nodeTypes.function.buildNode('range', 'bytes', { - lte: 8000, - }), - ]); - const actual = ast.fromKueryExpression('bytes >= 1000 and bytes <= 8000'); - expect(actual).to.eql(expected); - }); - - it('should support wildcards in field names', function () { - const expected = nodeTypes.function.buildNode('is', 'machine*', 'osx'); - const actual = ast.fromKueryExpression('machine*:osx'); - expect(actual).to.eql(expected); - }); - - it('should support wildcards in values', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'ba*'); - const actual = ast.fromKueryExpression('foo:ba*'); - expect(actual).to.eql(expected); - }); - - it('should create an exists "is" query when a field is given and "*" is the value', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', '*'); - const actual = ast.fromKueryExpression('foo:*'); - expect(actual).to.eql(expected); - }); - - it('should support nested queries indicated by curly braces', () => { - const expected = nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'foo') - ); - const actual = ast.fromKueryExpression('nestedField:{ childOfNested: foo }'); - expect(actual).to.eql(expected); - }); - - it('should support nested subqueries and subqueries inside nested queries', () => { - const expected = nodeTypes.function.buildNode( - 'and', - [ - nodeTypes.function.buildNode('is', 'response', '200'), - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'childOfNested', 'foo'), - nodeTypes.function.buildNode('is', 'childOfNested', 'bar'), - ]) - )]); - const actual = ast.fromKueryExpression('response:200 and nestedField:{ childOfNested:foo or childOfNested:bar }'); - expect(actual).to.eql(expected); - }); - - it('should support nested sub-queries inside paren groups', () => { - const expected = nodeTypes.function.buildNode( - 'and', - [ - nodeTypes.function.buildNode('is', 'response', '200'), - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'foo') - ), - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'bar') - ), - ]) - ]); - const actual = ast.fromKueryExpression('response:200 and ( nestedField:{ childOfNested:foo } or nestedField:{ childOfNested:bar } )'); - expect(actual).to.eql(expected); - }); - - it('should support nested groups inside other nested groups', () => { - const expected = nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode( - 'nested', - 'nestedChild', - nodeTypes.function.buildNode('is', 'doublyNestedChild', 'foo') - ) - ); - const actual = ast.fromKueryExpression('nestedField:{ nestedChild:{ doublyNestedChild:foo } }'); - expect(actual).to.eql(expected); - }); - }); - - describe('fromLiteralExpression', function () { - - it('should create literal nodes for unquoted values with correct primitive types', function () { - const stringLiteral = nodeTypes.literal.buildNode('foo'); - const booleanFalseLiteral = nodeTypes.literal.buildNode(false); - const booleanTrueLiteral = nodeTypes.literal.buildNode(true); - const numberLiteral = nodeTypes.literal.buildNode(42); - - expect(ast.fromLiteralExpression('foo')).to.eql(stringLiteral); - expect(ast.fromLiteralExpression('true')).to.eql(booleanTrueLiteral); - expect(ast.fromLiteralExpression('false')).to.eql(booleanFalseLiteral); - expect(ast.fromLiteralExpression('42')).to.eql(numberLiteral); - }); - - it('should allow escaping of special characters with a backslash', function () { - const expected = nodeTypes.literal.buildNode('\\():<>"*'); - // yo dawg - const actual = ast.fromLiteralExpression('\\\\\\(\\)\\:\\<\\>\\"\\*'); - expect(actual).to.eql(expected); - }); - - it('should support double quoted strings that do not need escapes except for quotes', function () { - const expected = nodeTypes.literal.buildNode('\\():<>"*'); - const actual = ast.fromLiteralExpression('"\\():<>\\"*"'); - expect(actual).to.eql(expected); - }); - - it('should support escaped backslashes inside quoted strings', function () { - const expected = nodeTypes.literal.buildNode('\\'); - const actual = ast.fromLiteralExpression('"\\\\"'); - expect(actual).to.eql(expected); - }); - - it('should detect wildcards and build wildcard AST nodes', function () { - const expected = nodeTypes.wildcard.buildNode('foo*bar'); - const actual = ast.fromLiteralExpression('foo*bar'); - expect(actual).to.eql(expected); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should return the given node type\'s ES query representation', function () { - const node = nodeTypes.function.buildNode('exists', 'response'); - const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern); - const result = ast.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an empty "and" function for undefined nodes and unknown node types', function () { - const expected = nodeTypes.function.toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); - - expect(ast.toElasticsearchQuery()).to.eql(expected); - - const noTypeNode = nodeTypes.function.buildNode('exists', 'foo'); - delete noTypeNode.type; - expect(ast.toElasticsearchQuery(noTypeNode)).to.eql(expected); - - const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo'); - unknownTypeNode.type = 'notValid'; - expect(ast.toElasticsearchQuery(unknownTypeNode)).to.eql(expected); - }); - - it('should return the given node type\'s ES query representation including a time zone parameter when one is provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); - const result = ast.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - }); - - describe('doesKueryExpressionHaveLuceneSyntaxError', function () { - it('should return true for Lucene ranges', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]'); - expect(result).to.eql(true); - }); - - it('should return false for KQL ranges', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar < 1'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene exists', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar'); - expect(result).to.eql(true); - }); - - it('should return false for KQL exists', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar:*'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene wildcards', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba?'); - expect(result).to.eql(true); - }); - - it('should return false for KQL wildcards', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba*'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene regex', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene fuzziness', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba~'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene proximity', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene boosting', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene + operator', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('+foo: bar'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene - operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('-foo: bar'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene && operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene || operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux'); - expect(result).to.eql(true); - }); - - it('should return true for mixed KQL/Lucene queries', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)'); - expect(result).to.eql(true); - }); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts deleted file mode 100644 index ef3d0ee828874..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { JsonObject } from '..'; - -/** - * WARNING: these typings are incomplete - */ - -export type KueryNode = any; - -export type DslQuery = any; - -export interface KueryParseOptions { - helpers: { - [key: string]: any; - }; - startRule: string; - allowLeadingWildcards: boolean; -} - -export function fromKueryExpression( - expression: string | DslQuery, - parseOptions?: Partial -): KueryNode; - -export function toElasticsearchQuery( - node: KueryNode, - indexPattern?: any, - config?: Record, - context?: Record -): JsonObject; - -export function doesKueryExpressionHaveLuceneSyntaxError(expression: string): boolean; diff --git a/packages/kbn-es-query/src/kuery/ast/index.d.ts b/packages/kbn-es-query/src/kuery/ast/index.d.ts deleted file mode 100644 index 9e68d01d046cc..0000000000000 --- a/packages/kbn-es-query/src/kuery/ast/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from '../ast/ast'; diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js b/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js deleted file mode 100644 index 7afa0fcce1bfe..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as geoBoundingBox from '../geo_bounding_box'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; -const params = { - bottomRight: { - lat: 50.73, - lon: -135.35 - }, - topLeft: { - lat: 73.12, - lon: -174.37 - } -}; - -describe('kuery functions', function () { - describe('geoBoundingBox', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('should return an "arguments" param', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - expect(result).to.only.have.keys('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - const { arguments: [ fieldName ] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'geo'); - }); - - it('arguments should contain the provided params as named arguments with "lat, lon" string values', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - const { arguments: [ , ...args ] } = result; - - args.map((param) => { - expect(param).to.have.property('type', 'namedArg'); - expect(['bottomRight', 'topLeft'].includes(param.name)).to.be(true); - expect(param.value.type).to.be('literal'); - - const expectedParam = params[param.name]; - const expectedLatLon = `${expectedParam.lat}, ${expectedParam.lon}`; - expect(param.value.value).to.be(expectedLatLon); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES geo_bounding_box query representing the given node', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37'); - expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35'); - }); - - it('should return an ES geo_bounding_box query without an index pattern', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37'); - expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35'); - }); - - it('should use the ignore_unmapped parameter', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); - expect(result.geo_bounding_box.ignore_unmapped).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params); - expect(geoBoundingBox.toElasticsearchQuery) - .withArgs(node, indexPattern).to.throwException(/Geo bounding box query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box).to.have.property('nestedField.geo'); - }); - - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js b/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js deleted file mode 100644 index c1f2fae0bb3e1..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as geoPolygon from '../geo_polygon'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - - -let indexPattern; -const points = [ - { - lat: 69.77, - lon: -171.56 - }, - { - lat: 50.06, - lon: -169.10 - }, - { - lat: 69.16, - lon: -125.85 - } -]; - -describe('kuery functions', function () { - - describe('geoPolygon', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('should return an "arguments" param', function () { - const result = geoPolygon.buildNodeParams('geo', points); - expect(result).to.only.have.keys('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const result = geoPolygon.buildNodeParams('geo', points); - const { arguments: [ fieldName ] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'geo'); - }); - - it('arguments should contain the provided points literal "lat, lon" string values', function () { - const result = geoPolygon.buildNodeParams('geo', points); - const { arguments: [ , ...args ] } = result; - - args.forEach((param, index) => { - expect(param).to.have.property('type', 'literal'); - const expectedPoint = points[index]; - const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`; - expect(param.value).to.be(expectedLatLon); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES geo_polygon query representing the given node', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon.geo).to.have.property('points'); - - result.geo_polygon.geo.points.forEach((point, index) => { - const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; - expect(point).to.be(expectedLatLon); - }); - }); - - it('should return an ES geo_polygon query without an index pattern', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon.geo).to.have.property('points'); - - result.geo_polygon.geo.points.forEach((point, index) => { - const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; - expect(point).to.be(expectedLatLon); - }); - }); - - it('should use the ignore_unmapped parameter', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node, indexPattern); - expect(result.geo_polygon.ignore_unmapped).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points); - expect(geoPolygon.toElasticsearchQuery) - .withArgs(node, indexPattern).to.throwException(/Geo polygon query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon).to.have.property('nestedField.geo'); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js b/packages/kbn-es-query/src/kuery/functions/__tests__/is.js deleted file mode 100644 index b2f3d7ec16a65..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as is from '../is'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery functions', function () { - - describe('is', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('fieldName and value should be required arguments', function () { - expect(is.buildNodeParams).to.throwException(/fieldName is a required argument/); - expect(is.buildNodeParams).withArgs('foo').to.throwException(/value is a required argument/); - }); - - it('arguments should contain the provided fieldName and value as literals', function () { - const { arguments: [fieldName, value] } = is.buildNodeParams('response', 200); - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'response'); - - expect(value).to.have.property('type', 'literal'); - expect(value).to.have.property('value', 200); - }); - - it('should detect wildcards in the provided arguments', function () { - const { arguments: [fieldName, value] } = is.buildNodeParams('machine*', 'win*'); - - expect(fieldName).to.have.property('type', 'wildcard'); - expect(value).to.have.property('type', 'wildcard'); - }); - - it('should default to a non-phrase query', function () { - const { arguments: [, , isPhrase] } = is.buildNodeParams('response', 200); - expect(isPhrase.value).to.be(false); - }); - - it('should allow specification of a phrase query', function () { - const { arguments: [, , isPhrase] } = is.buildNodeParams('response', 200, true); - expect(isPhrase.value).to.be(true); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES match_all query when fieldName and value are both "*"', function () { - const expected = { - match_all: {} - }; - - const node = nodeTypes.function.buildNode('is', '*', '*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES multi_match query using default_field when fieldName is null', function () { - const expected = { - multi_match: { - query: 200, - type: 'best_fields', - lenient: true, - } - }; - - const node = nodeTypes.function.buildNode('is', null, 200); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES query_string query using default_field when fieldName is null and value contains a wildcard', function () { - const expected = { - query_string: { - query: 'jpg*', - } - }; - - const node = nodeTypes.function.buildNode('is', null, 'jpg*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES bool query with a sub-query for each field when fieldName is "*"', function () { - const node = nodeTypes.function.buildNode('is', '*', 200); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('bool'); - expect(result.bool.should).to.have.length(indexPattern.fields.length); - }); - - it('should return an ES exists query when value is "*"', function () { - const expected = { - bool: { - should: [ - { exists: { field: 'extension' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', '*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES match query when a concrete fieldName and value are provided', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES match query when a concrete fieldName and value are provided without an index pattern', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery(node); - expect(result).to.eql(expected); - }); - - it('should support creation of phrase queries', function () { - const expected = { - bool: { - should: [ - { match_phrase: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg', true); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should create a query_string query for wildcard values', function () { - const expected = { - bool: { - should: [ - { - query_string: { - fields: ['extension'], - query: 'jpg*' - } - }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support scripted fields', function () { - const node = nodeTypes.function.buildNode('is', 'script string', 'foo'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result.bool.should[0]).to.have.key('script'); - }); - - it('should support date fields without a dateFormat provided', function () { - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: '2018-04-03T19:04:17', - lte: '2018-04-03T19:04:17', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support date fields with a dateFormat provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: '2018-04-03T19:04:17', - lte: '2018-04-03T19:04:17', - time_zone: 'America/Phoenix', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const result = is.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - it('should use a provided nested context to create a full field name', function () { - const expected = { - bool: { - should: [ - { match: { 'nestedField.extension': 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.eql(expected); - }); - - it('should support wildcard field names', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should automatically add a nested query when a wildcard field name covers a nested field', () => { - const expected = { - bool: { - should: [ - { - nested: { - path: 'nestedField.nestedChild', - query: { - match: { - 'nestedField.nestedChild.doublyNestedChild': 'foo' - } - }, - score_mode: 'none' - } - } - ], - minimum_should_match: 1 - } - }; - - - const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js b/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js deleted file mode 100644 index dae15979a161c..0000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { nodeTypes } from '../../../node_types'; -import indexPatternResponse from '../../../../__fixtures__/index_pattern_response.json'; -import { getFullFieldNameNode } from '../../utils/get_full_field_name_node'; - -let indexPattern; - -describe('getFullFieldNameNode', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - it('should return unchanged name node if no nested path is passed in', () => { - const nameNode = nodeTypes.literal.buildNode('notNested'); - const result = getFullFieldNameNode(nameNode, indexPattern); - expect(result).to.eql(nameNode); - }); - - it('should add the nested path if it is valid according to the index pattern', () => { - const nameNode = nodeTypes.literal.buildNode('child'); - const result = getFullFieldNameNode(nameNode, indexPattern, 'nestedField'); - expect(result).to.eql(nodeTypes.literal.buildNode('nestedField.child')); - }); - - it('should throw an error if a path is provided for a non-nested field', () => { - const nameNode = nodeTypes.literal.buildNode('os'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern, 'machine') - .to - .throwException(/machine.os is not a nested field but is in nested group "machine" in the KQL expression/); - }); - - it('should throw an error if a nested field is not passed with a path', () => { - const nameNode = nodeTypes.literal.buildNode('nestedField.child'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern) - .to - .throwException(/nestedField.child is a nested field, but is not in a nested group in the KQL expression./); - }); - - it('should throw an error if a nested field is passed with the wrong path', () => { - const nameNode = nodeTypes.literal.buildNode('nestedChild.doublyNestedChild'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern, 'nestedField') - .to - // eslint-disable-next-line max-len - .throwException(/Nested field nestedField.nestedChild.doublyNestedChild is being queried with the incorrect nested path. The correct path is nestedField.nestedChild/); - }); - - it('should skip error checking for wildcard names', () => { - const nameNode = nodeTypes.wildcard.buildNode('nested*'); - const result = getFullFieldNameNode(nameNode, indexPattern); - expect(result).to.eql(nameNode); - }); - - it('should skip error checking if no index pattern is passed in', () => { - const nameNode = nodeTypes.literal.buildNode('os'); - expect(getFullFieldNameNode) - .withArgs(nameNode, null, 'machine') - .to - .not - .throwException(); - - const result = getFullFieldNameNode(nameNode, null, 'machine'); - expect(result).to.eql(nodeTypes.literal.buildNode('machine.os')); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js deleted file mode 100644 index de00c083fc830..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as functionType from '../function'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import * as isFunction from '../../functions/is'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -import { nodeTypes } from '../../node_types'; - -describe('kuery node types', function () { - - describe('function', function () { - - let indexPattern; - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNode', function () { - - it('should return a node representing the given kuery function', function () { - const result = functionType.buildNode('is', 'extension', 'jpg'); - expect(result).to.have.property('type', 'function'); - expect(result).to.have.property('function', 'is'); - expect(result).to.have.property('arguments'); - }); - - }); - - describe('buildNodeWithArgumentNodes', function () { - - it('should return a function node with the given argument list untouched', function () { - const fieldNameLiteral = nodeTypes.literal.buildNode('extension'); - const valueLiteral = nodeTypes.literal.buildNode('jpg'); - const argumentNodes = [fieldNameLiteral, valueLiteral]; - const result = functionType.buildNodeWithArgumentNodes('is', argumentNodes); - - expect(result).to.have.property('type', 'function'); - expect(result).to.have.property('function', 'is'); - expect(result).to.have.property('arguments'); - expect(result.arguments).to.be(argumentNodes); - expect(result.arguments).to.eql(argumentNodes); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the given function type\'s ES query representation', function () { - const node = functionType.buildNode('is', 'extension', 'jpg'); - const expected = isFunction.toElasticsearchQuery(node, indexPattern); - const result = functionType.toElasticsearchQuery(node, indexPattern); - expect(_.isEqual(expected, result)).to.be(true); - }); - - }); - - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js deleted file mode 100644 index cfb8f6d5274db..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as namedArg from '../named_arg'; -import { nodeTypes } from '../../node_types'; - -describe('kuery node types', function () { - - describe('named arg', function () { - - describe('buildNode', function () { - - it('should return a node representing a named argument with the given value', function () { - const result = namedArg.buildNode('fieldName', 'foo'); - expect(result).to.have.property('type', 'namedArg'); - expect(result).to.have.property('name', 'fieldName'); - expect(result).to.have.property('value'); - - const literalValue = result.value; - expect(literalValue).to.have.property('type', 'literal'); - expect(literalValue).to.have.property('value', 'foo'); - }); - - it('should support literal nodes as values', function () { - const value = nodeTypes.literal.buildNode('foo'); - const result = namedArg.buildNode('fieldName', value); - expect(result.value).to.be(value); - expect(result.value).to.eql(value); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the argument value represented by the given node', function () { - const node = namedArg.buildNode('fieldName', 'foo'); - const result = namedArg.toElasticsearchQuery(node); - expect(result).to.be('foo'); - }); - - }); - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js deleted file mode 100644 index 0c4379378c6d6..0000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import * as wildcard from '../wildcard'; - -describe('kuery node types', function () { - - describe('wildcard', function () { - - describe('buildNode', function () { - - it('should accept a string argument representing a wildcard string', function () { - const wildcardValue = `foo${wildcard.wildcardSymbol}bar`; - const result = wildcard.buildNode(wildcardValue); - expect(result).to.have.property('type', 'wildcard'); - expect(result).to.have.property('value', wildcardValue); - }); - - it('should accept and parse a wildcard string', function () { - const result = wildcard.buildNode('foo*bar'); - expect(result).to.have.property('type', 'wildcard'); - expect(result.value).to.be(`foo${wildcard.wildcardSymbol}bar`); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the string representation of the wildcard literal', function () { - const node = wildcard.buildNode('foo*bar'); - const result = wildcard.toElasticsearchQuery(node); - expect(result).to.be('foo*bar'); - }); - - }); - - describe('toQueryStringQuery', function () { - - it('should return the string representation of the wildcard literal', function () { - const node = wildcard.buildNode('foo*bar'); - const result = wildcard.toQueryStringQuery(node); - expect(result).to.be('foo*bar'); - }); - - it('should escape query_string query special characters other than wildcard', function () { - const node = wildcard.buildNode('+foo*bar'); - const result = wildcard.toQueryStringQuery(node); - expect(result).to.be('\\+foo*bar'); - }); - - }); - - describe('test', function () { - - it('should return a boolean indicating whether the string matches the given wildcard node', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.test(node, 'foobar')).to.be(true); - expect(wildcard.test(node, 'foobazbar')).to.be(true); - expect(wildcard.test(node, 'foobar')).to.be(true); - - expect(wildcard.test(node, 'fooqux')).to.be(false); - expect(wildcard.test(node, 'bazbar')).to.be(false); - }); - - it('should return a true even when the string has newlines or tabs', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.test(node, 'foo\nbar')).to.be(true); - expect(wildcard.test(node, 'foo\tbar')).to.be(true); - }); - }); - - describe('hasLeadingWildcard', function () { - it('should determine whether a wildcard node contains a leading wildcard', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.hasLeadingWildcard(node)).to.be(false); - - const leadingWildcardNode = wildcard.buildNode('*foobar'); - expect(wildcard.hasLeadingWildcard(leadingWildcardNode)).to.be(true); - }); - - // Lone wildcards become exists queries, so we aren't worried about their performance - it('should not consider a lone wildcard to be a leading wildcard', function () { - const leadingWildcardNode = wildcard.buildNode('*'); - expect(wildcard.hasLeadingWildcard(leadingWildcardNode)).to.be(false); - }); - }); - - }); - -}); diff --git a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js deleted file mode 100644 index 6deaccadfdb76..0000000000000 --- a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { getTimeZoneFromSettings } from '../get_time_zone_from_settings'; - -describe('get timezone from settings', function () { - - it('should return the config timezone if the time zone is set', function () { - const result = getTimeZoneFromSettings('America/Chicago'); - expect(result).to.eql('America/Chicago'); - }); - - it('should return the system timezone if the time zone is set to "Browser"', function () { - const result = getTimeZoneFromSettings('Browser'); - expect(result).to.not.equal('Browser'); - }); - -}); - diff --git a/packages/kbn-es-query/src/utils/filters.js b/packages/kbn-es-query/src/utils/filters.js deleted file mode 100644 index 6e4f5c342688c..0000000000000 --- a/packages/kbn-es-query/src/utils/filters.js +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { pick, get, reduce, map } from 'lodash'; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export const getConvertedValueForField = (field, value) => { - if (typeof value !== 'boolean' && field.type === 'boolean') { - if ([1, 'true'].includes(value)) { - return true; - } else if ([0, 'false'].includes(value)) { - return false; - } else { - throw new Error(`${value} is not a valid boolean value for boolean field ${field.name}`); - } - } - return value; -}; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export const buildInlineScriptForPhraseFilter = (scriptedField) => { - // We must wrap painless scripts in a lambda in case they're more than a simple expression - if (scriptedField.lang === 'painless') { - return ( - `boolean compare(Supplier s, def v) {return s.get() == v;}` + - `compare(() -> { ${scriptedField.script} }, params.value);` - ); - } else { - return `(${scriptedField.script}) == value`; - } -}; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export function getPhraseScript(field, value) { - const convertedValue = getConvertedValueForField(field, value); - const script = buildInlineScriptForPhraseFilter(field); - - return { - script: { - source: script, - lang: field.lang, - params: { - value: convertedValue, - }, - }, - }; -} - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/range_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'kuery' into new platform - * */ -export function getRangeScript(field, params) { - const operators = { - gt: '>', - gte: '>=', - lte: '<=', - lt: '<', - }; - const comparators = { - gt: 'boolean gt(Supplier s, def v) {return s.get() > v}', - gte: 'boolean gte(Supplier s, def v) {return s.get() >= v}', - lte: 'boolean lte(Supplier s, def v) {return s.get() <= v}', - lt: 'boolean lt(Supplier s, def v) {return s.get() < v}', - }; - - const dateComparators = { - gt: 'boolean gt(Supplier s, def v) {return s.get().toInstant().isAfter(Instant.parse(v))}', - gte: 'boolean gte(Supplier s, def v) {return !s.get().toInstant().isBefore(Instant.parse(v))}', - lte: 'boolean lte(Supplier s, def v) {return !s.get().toInstant().isAfter(Instant.parse(v))}', - lt: 'boolean lt(Supplier s, def v) {return s.get().toInstant().isBefore(Instant.parse(v))}', - }; - - const knownParams = pick(params, (val, key) => { - return key in operators; - }); - let script = map(knownParams, (val, key) => { - return '(' + field.script + ')' + get(operators, key) + key; - }).join(' && '); - - // We must wrap painless scripts in a lambda in case they're more than a simple expression - if (field.lang === 'painless') { - const comp = field.type === 'date' ? dateComparators : comparators; - const currentComparators = reduce( - knownParams, - (acc, val, key) => acc.concat(get(comp, key)), - [] - ).join(' '); - - const comparisons = map(knownParams, (val, key) => { - return `${key}(() -> { ${field.script} }, params.${key})`; - }).join(' && '); - - script = `${currentComparators}${comparisons}`; - } - - return { - script: { - source: script, - params: knownParams, - lang: field.lang, - }, - }; -} diff --git a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js deleted file mode 100644 index 1a06941ece127..0000000000000 --- a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import moment from 'moment-timezone'; -const detectedTimezone = moment.tz.guess(); - -export function getTimeZoneFromSettings(dateFormatTZ) { - if (dateFormatTZ === 'Browser') { - return detectedTimezone; - } - return dateFormatTZ; -} diff --git a/packages/kbn-es-query/src/utils/index.js b/packages/kbn-es-query/src/utils/index.js deleted file mode 100644 index 27f51c1f44cf2..0000000000000 --- a/packages/kbn-es-query/src/utils/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './get_time_zone_from_settings'; diff --git a/packages/kbn-es-query/tasks/build_cli.js b/packages/kbn-es-query/tasks/build_cli.js deleted file mode 100644 index 2a43c4d10e007..0000000000000 --- a/packages/kbn-es-query/tasks/build_cli.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { resolve } = require('path'); - -const getopts = require('getopts'); -const del = require('del'); -const supportsColor = require('supports-color'); -const { ToolingLog, withProcRunner, pickLevelFromFlags } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -const unknownFlags = []; -const flags = getopts(process.argv, { - boolean: ['watch', 'help', 'source-maps'], - unknown(name) { - unknownFlags.push(name); - }, -}); - -const log = new ToolingLog({ - level: pickLevelFromFlags(flags), - writeTo: process.stdout, -}); - -if (unknownFlags.length) { - log.error(`Unknown flag(s): ${unknownFlags.join(', ')}`); - flags.help = true; - process.exitCode = 1; -} - -if (flags.help) { - log.info(` - Simple build tool for @kbn/es-query package - - --watch Run in watch mode - --source-maps Include sourcemaps - --help Show this message - `); - process.exit(); -} - -withProcRunner(log, async proc => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['public', 'server'].map(subTask => - proc.run(padRight(12, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.js,.ts,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-map', 'inline'] : []), - ], - wait: true, - cwd, - env: { - ...env, - BABEL_ENV: subTask, - }, - }) - ), - ]); - - log.success('Complete'); -}).catch(error => { - log.error(error); - process.exit(1); -}); diff --git a/packages/kbn-es-query/tsconfig.browser.json b/packages/kbn-es-query/tsconfig.browser.json deleted file mode 100644 index 4a91407471266..0000000000000 --- a/packages/kbn-es-query/tsconfig.browser.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.browser.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target/public" - }, - "include": [ - "index.d.ts", - "src/**/*.ts" - ] -} diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json deleted file mode 100644 index 05f51bbccd2ff..0000000000000 --- a/packages/kbn-es-query/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target/server" - }, - "include": [ - "index.d.ts", - "src/**/*.ts" - ] -} diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 16a634b2d3287..30a98c9046ff5 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -846,7 +846,7 @@ export class SavedObjectsClient { bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 4c4f321695d70..13a132ab9dd67 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { fromKueryExpression } from '@kbn/es-query'; +import { esKuery } from '../../../../../plugins/data/server'; import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; @@ -64,7 +64,7 @@ describe('Filter Utils', () => { test('Validate a simple filter', () => { expect( validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockMappings) - ).toEqual(fromKueryExpression('foo.title: "best"')); + ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); }); test('Assemble filter kuery node saved object attributes with one saved object type', () => { expect( @@ -74,7 +74,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); @@ -88,7 +88,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); @@ -102,7 +102,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '((type: bar and updatedAt: 5678654567) or (type: foo and updatedAt: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)' ) ); @@ -130,7 +130,7 @@ describe('Filter Utils', () => { describe('#validateFilterKueryNode', () => { test('Validate filter query through KueryNode - happy path', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -185,7 +185,7 @@ describe('Filter Utils', () => { test('Return Error if key is not wrapper by a saved object type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -240,7 +240,7 @@ describe('Filter Utils', () => { test('Return Error if key of a saved object type is not wrapped with attributes', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' ), ['foo'], @@ -297,7 +297,7 @@ describe('Filter Utils', () => { test('Return Error if filter is not using an allowed type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -352,7 +352,7 @@ describe('Filter Utils', () => { test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -408,7 +408,7 @@ describe('Filter Utils', () => { test('Return Error if filter is using an non-existing key null key', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression('foo.attributes.description: hello AND bye'), + esKuery.fromKueryExpression('foo.attributes.description: hello AND bye'), ['foo'], mockMappings ); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index e331d3eff990f..3cf499de541ee 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -17,18 +17,18 @@ * under the License. */ -import { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query'; import { get, set } from 'lodash'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; +import { esKuery } from '../../../../../plugins/data/server'; export const validateConvertFilterToKueryNode = ( allowedTypes: string[], filter: string, indexMapping: IndexMapping -): KueryNode => { +): esKuery.KueryNode | undefined => { if (filter && filter.length > 0 && indexMapping) { - const filterKueryNode = fromKueryExpression(filter); + const filterKueryNode = esKuery.fromKueryExpression(filter); const validationFilterKuery = validateFilterKueryNode( filterKueryNode, @@ -54,7 +54,7 @@ export const validateConvertFilterToKueryNode = ( validationFilterKuery.forEach(item => { const path: string[] = item.astPath.length === 0 ? [] : item.astPath.split('.'); - const existingKueryNode: KueryNode = + const existingKueryNode: esKuery.KueryNode = path.length === 0 ? filterKueryNode : get(filterKueryNode, path); if (item.isSavedObjectAttr) { existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1]; @@ -63,8 +63,8 @@ export const validateConvertFilterToKueryNode = ( set( filterKueryNode, path, - nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', 'type', itemType[0]), + esKuery.nodeTypes.function.buildNode('and', [ + esKuery.nodeTypes.function.buildNode('is', 'type', itemType[0]), existingKueryNode, ]) ); @@ -79,7 +79,6 @@ export const validateConvertFilterToKueryNode = ( }); return filterKueryNode; } - return null; }; interface ValidateFilterKueryNode { @@ -91,41 +90,44 @@ interface ValidateFilterKueryNode { } export const validateFilterKueryNode = ( - astFilter: KueryNode, + astFilter: esKuery.KueryNode, types: string[], indexMapping: IndexMapping, storeValue: boolean = false, path: string = 'arguments' ): ValidateFilterKueryNode[] => { - return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { - if (ast.arguments) { - const myPath = `${path}.${index}`; - return [ - ...kueryNode, - ...validateFilterKueryNode( - ast, - types, - indexMapping, - ast.type === 'function' && ['is', 'range'].includes(ast.function), - `${myPath}.arguments` - ), - ]; - } - if (storeValue && index === 0) { - const splitPath = path.split('.'); - return [ - ...kueryNode, - { - astPath: splitPath.slice(0, splitPath.length - 1).join('.'), - error: hasFilterKeyError(ast.value, types, indexMapping), - isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), - key: ast.value, - type: getType(ast.value), - }, - ]; - } - return kueryNode; - }, []); + return astFilter.arguments.reduce( + (kueryNode: string[], ast: esKuery.KueryNode, index: number) => { + if (ast.arguments) { + const myPath = `${path}.${index}`; + return [ + ...kueryNode, + ...validateFilterKueryNode( + ast, + types, + indexMapping, + ast.type === 'function' && ['is', 'range'].includes(ast.function), + `${myPath}.arguments` + ), + ]; + } + if (storeValue && index === 0) { + const splitPath = path.split('.'); + return [ + ...kueryNode, + { + astPath: splitPath.slice(0, splitPath.length - 1).join('.'), + error: hasFilterKeyError(ast.value, types, indexMapping), + isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), + key: ast.value, + type: getType(ast.value), + }, + ]; + } + return kueryNode; + }, + [] + ); }; const getType = (key: string | undefined | null) => diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 79a3e573ab98c..3d81c2c2efd52 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -1289,8 +1289,7 @@ describe('SavedObjectsRepository', () => { type: 'foo', id: '1', }, - indexPattern: undefined, - kueryNode: null, + kueryNode: undefined, }; await savedObjectsRepository.find(relevantOpts); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 51d4a8ad50ad6..e8f1fb16461c1 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -448,11 +448,11 @@ export class SavedObjectsRepository { } let kueryNode; + try { - kueryNode = - filter && filter !== '' - ? validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings) - : null; + if (filter) { + kueryNode = validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings); + } } catch (e) { if (e.name === 'KQLSyntaxError') { throw SavedObjectsErrorHelpers.createBadRequestError('KQLSyntaxError: ' + e.message); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index bee35b899d83c..cfeb258c2f03b 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { toElasticsearchQuery, KueryNode } from '@kbn/es-query'; +import { esKuery } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; @@ -91,7 +91,7 @@ interface QueryParams { searchFields?: string[]; defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; - kueryNode?: KueryNode; + kueryNode?: esKuery.KueryNode; } /** @@ -111,7 +111,7 @@ export function getQueryParams({ const types = getTypes(mappings, type); const bool: any = { filter: [ - ...(kueryNode != null ? [toElasticsearchQuery(kueryNode)] : []), + ...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []), { bool: { must: hasReference diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 868ca51a76eab..f2bbc3ef564a1 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -17,13 +17,13 @@ * under the License. */ -import { KueryNode } from '@kbn/es-query'; import Boom from 'boom'; import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; +import { esKuery } from '../../../../../../plugins/data/server'; interface GetSearchDslOptions { type: string | string[]; @@ -37,7 +37,7 @@ interface GetSearchDslOptions { type: string; id: string; }; - kueryNode?: KueryNode; + kueryNode?: esKuery.KueryNode; } export function getSearchDsl( diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index 1bf8ac086d341..ed3c2413b0eb4 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -18,11 +18,8 @@ */ import dateMath from '@elastic/datemath'; -import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query'; - import classNames from 'classnames'; import React, { useState } from 'react'; - import { EuiButton, EuiFlexGroup, @@ -42,9 +39,9 @@ import { Query, PersistedLog, getQueryLog, + esKuery, } from '../../../../../../../plugins/data/public'; import { useKibana, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; - import { IndexPattern } from '../../../index_patterns'; import { QueryBarInput } from './query_bar_input'; @@ -300,7 +297,7 @@ function QueryBarTopRowUI(props: Props) { language === 'kuery' && typeof query === 'string' && (!storage || !storage.get('kibana.luceneSyntaxWarningOptOut')) && - doesKueryExpressionHaveLuceneSyntaxError(query) + esKuery.doesKueryExpressionHaveLuceneSyntaxError(query) ) { const toast = notifications!.toasts.addWarning({ title: intl.formatMessage({ diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts index 74111bf794877..14cd3d0083e6a 100644 --- a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts +++ b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts @@ -83,7 +83,7 @@ export function getTimelionRequestHandler(dependencies: TimelionVisualizationDep sheet: [expression], extended: { es: { - filter: esQuery.buildEsQuery(null, query, filters, esQueryConfigs), + filter: esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs), }, }, time: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 2b42c22ad7c43..1d42b77336933 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -22,7 +22,6 @@ import React, { Component } from 'react'; import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; -import { fromKueryExpression } from '@kbn/es-query'; import { VisEditorVisualization } from './vis_editor_visualization'; import { Visualization } from './visualization'; import { VisPicker } from './vis_picker'; @@ -30,6 +29,7 @@ import { PanelConfig } from './panel_config'; import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../common/extract_index_patterns'; +import { esKuery } from '../../../../../plugins/data/public'; import { npStart } from 'ui/new_platform'; @@ -88,7 +88,7 @@ export class VisEditor extends Component { if (filterQuery && filterQuery.language === 'kuery') { try { const queryOptions = this.coreContext.uiSettings.get('query:allowLeadingWildcards'); - fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); + esKuery.fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); } catch (error) { return false; } diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts index 83ae31bf87400..26380bf2b9d94 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts @@ -49,7 +49,7 @@ export function createVegaRequestHandler({ timeCache.setTimeRange(timeRange); const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); - const filtersDsl = esQuery.buildEsQuery(null, query, filters, esQueryConfigs); + const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); const vp = new VegaParser(visParams.spec, searchCache, timeCache, filtersDsl, serviceSettings); return vp.parseAsync(); diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts index 3db23051b6ced..405754ffcb572 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts @@ -18,7 +18,7 @@ */ import { buildEsQuery } from './build_es_query'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { luceneStringToDsl } from './lucene_string_to_dsl'; import { decorateQuery } from './decorate_query'; import { IIndexPattern } from '../../index_patterns'; diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts index b754496793660..e4f5f1f9e216c 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts @@ -41,7 +41,7 @@ export interface EsQueryConfig { * config contains dateformat:tz */ export function buildEsQuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queries: Query | Query[], filters: Filter | Filter[], config: EsQueryConfig = { diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index 6e03c665290ae..669c5a62af726 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -31,9 +31,8 @@ describe('filterMatchesIndex', () => { it('should return true if no index pattern is passed', () => { const filter = { meta: { index: 'foo', key: 'bar' } } as Filter; - const indexPattern = null; - expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + expect(filterMatchesIndex(filter, undefined)).toBe(true); }); it('should return true if the filter key matches a field name', () => { diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 9b68f5088c447..a9cd3d8b7ba26 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -25,7 +25,7 @@ import { Filter } from '../filters'; * this to check if `filter.meta.index` matches `indexPattern.id` instead, but that's a breaking * change. */ -export function filterMatchesIndex(filter: Filter, indexPattern: IIndexPattern | null) { +export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern | null) { if (!filter.meta?.key || !indexPattern) { return true; } diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts index 1e0957d816590..e33040485bf47 100644 --- a/src/plugins/data/common/es_query/es_query/from_filters.ts +++ b/src/plugins/data/common/es_query/es_query/from_filters.ts @@ -54,7 +54,7 @@ const translateToQuery = (filter: Filter) => { export const buildQueryFromFilters = ( filters: Filter[] = [], - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex: boolean = false ) => { filters = filters.filter(filter => filter && !isFilterDisabled(filter)); diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts b/src/plugins/data/common/es_query/es_query/from_kuery.test.ts index 000815b51f620..4574cd5ffd0cb 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.test.ts @@ -18,7 +18,7 @@ */ import { buildQueryFromKuery } from './from_kuery'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { IIndexPattern } from '../../index_patterns'; import { fields } from '../../index_patterns/mocks'; import { Query } from '../../query/types'; @@ -30,7 +30,7 @@ describe('build query', () => { describe('buildQueryFromKuery', () => { test('should return the parameters of an Elasticsearch bool query', () => { - const result = buildQueryFromKuery(null, [], true); + const result = buildQueryFromKuery(undefined, [], true); const expected = { must: [], filter: [], diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/src/plugins/data/common/es_query/es_query/from_kuery.ts index f91c3d97b95b4..f4ec0fe0b34c5 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.ts @@ -17,12 +17,12 @@ * under the License. */ -import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery'; import { IIndexPattern } from '../../index_patterns'; import { Query } from '../../query/types'; export function buildQueryFromKuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queries: Query[] = [], allowLeadingWildcards: boolean = false, dateFormatTZ?: string @@ -35,22 +35,20 @@ export function buildQueryFromKuery( } function buildQuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queryASTs: KueryNode[], config: Record = {} ) { - const compoundQueryAST: KueryNode = nodeTypes.function.buildNode('and', queryASTs); - const kueryQuery: Record = toElasticsearchQuery( - compoundQueryAST, - indexPattern, - config - ); + const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs); + const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern, config); - return { - must: [], - filter: [], - should: [], - must_not: [], - ...kueryQuery.bool, - }; + return Object.assign( + { + must: [], + filter: [], + should: [], + must_not: [], + }, + kueryQuery.bool + ); } diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts index 4617ee1a1c43d..e01240da87543 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts @@ -40,7 +40,7 @@ describe('migrateFilter', function() { } as unknown) as PhraseFilter; it('should migrate match filters of type phrase', function() { - const migratedFilter = migrateFilter(oldMatchPhraseFilter, null); + const migratedFilter = migrateFilter(oldMatchPhraseFilter, undefined); expect(isEqual(migratedFilter, newMatchPhraseFilter)).toBe(true); }); @@ -48,7 +48,7 @@ describe('migrateFilter', function() { it('should not modify the original filter', function() { const oldMatchPhraseFilterCopy = clone(oldMatchPhraseFilter, true); - migrateFilter(oldMatchPhraseFilter, null); + migrateFilter(oldMatchPhraseFilter, undefined); expect(isEqual(oldMatchPhraseFilter, oldMatchPhraseFilterCopy)).toBe(true); }); @@ -57,7 +57,7 @@ describe('migrateFilter', function() { const originalFilter = { match_all: {}, } as MatchAllFilter; - const migratedFilter = migrateFilter(originalFilter, null); + const migratedFilter = migrateFilter(originalFilter, undefined); expect(migratedFilter).toBe(originalFilter); expect(isEqual(migratedFilter, originalFilter)).toBe(true); diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts index 258ab9e703131..fdc40768ebe41 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts @@ -43,7 +43,7 @@ function isMatchPhraseFilter(filter: any): filter is DeprecatedMatchPhraseFilter return Boolean(fieldName && get(filter, ['match', fieldName, 'type']) === 'phrase'); } -export function migrateFilter(filter: Filter, indexPattern: IIndexPattern | null) { +export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) { if (isMatchPhraseFilter(filter)) { const fieldName = Object.keys(filter.match)[0]; const params: Record = get(filter, ['match', fieldName]); diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index 56eb45c4b1dca..937fe09903b6b 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -18,6 +18,7 @@ */ import * as esQuery from './es_query'; import * as esFilters from './filters'; +import * as esKuery from './kuery'; import * as utils from './utils'; -export { esFilters, esQuery, utils }; +export { esFilters, esQuery, utils, esKuery }; diff --git a/packages/kbn-es-query/src/kuery/ast/kuery.js b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/kuery.js rename to src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts new file mode 100644 index 0000000000000..e441420760475 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts @@ -0,0 +1,421 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + fromKueryExpression, + fromLiteralExpression, + toElasticsearchQuery, + doesKueryExpressionHaveLuceneSyntaxError, +} from './ast'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import { KueryNode } from '../types'; + +describe('kuery AST API', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('fromKueryExpression', () => { + test('should return a match all "is" function for whitespace', () => { + const expected = nodeTypes.function.buildNode('is', '*', '*'); + const actual = fromKueryExpression(' '); + expect(actual).toEqual(expected); + }); + + test('should return an "is" function with a null field for single literals', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo'); + const actual = fromKueryExpression('foo'); + expect(actual).toEqual(expected); + }); + + test('should ignore extraneous whitespace at the beginning and end of the query', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo'); + const actual = fromKueryExpression(' foo '); + expect(actual).toEqual(expected); + }); + + test('should not split on whitespace', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo bar'); + const actual = fromKueryExpression('foo bar'); + expect(actual).toEqual(expected); + }); + + test('should support "and" as a binary operator', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]); + const actual = fromKueryExpression('foo and bar'); + expect(actual).toEqual(expected); + }); + + test('should support "or" as a binary operator', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]); + const actual = fromKueryExpression('foo or bar'); + expect(actual).toEqual(expected); + }); + + test('should support negation of queries with a "not" prefix', () => { + const expected = nodeTypes.function.buildNode( + 'not', + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]) + ); + const actual = fromKueryExpression('not (foo or bar)'); + expect(actual).toEqual(expected); + }); + + test('"and" should have a higher precedence than "or"', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', null, 'bar'), + nodeTypes.function.buildNode('is', null, 'baz'), + ]), + nodeTypes.function.buildNode('is', null, 'qux'), + ]), + ]); + const actual = fromKueryExpression('foo or bar and baz or qux'); + expect(actual).toEqual(expected); + }); + + test('should support grouping to override default precedence', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]), + nodeTypes.function.buildNode('is', null, 'baz'), + ]); + const actual = fromKueryExpression('(foo or bar) and baz'); + expect(actual).toEqual(expected); + }); + + test('should support matching against specific fields', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar'); + const actual = fromKueryExpression('foo:bar'); + expect(actual).toEqual(expected); + }); + + test('should also not split on whitespace when matching specific fields', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz'); + const actual = fromKueryExpression('foo:bar baz'); + expect(actual).toEqual(expected); + }); + + test('should treat quoted values as phrases', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz', true); + const actual = fromKueryExpression('foo:"bar baz"'); + expect(actual).toEqual(expected); + }); + + test('should support a shorthand for matching multiple values against a single field', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'foo', 'bar'), + nodeTypes.function.buildNode('is', 'foo', 'baz'), + ]); + const actual = fromKueryExpression('foo:(bar or baz)'); + expect(actual).toEqual(expected); + }); + + test('should support "and" and "not" operators and grouping in the shorthand as well', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'foo', 'bar'), + nodeTypes.function.buildNode('is', 'foo', 'baz'), + ]), + nodeTypes.function.buildNode('not', nodeTypes.function.buildNode('is', 'foo', 'qux')), + ]); + const actual = fromKueryExpression('foo:((bar or baz) and not qux)'); + expect(actual).toEqual(expected); + }); + + test('should support exclusive range operators', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('range', 'bytes', { + gt: 1000, + }), + nodeTypes.function.buildNode('range', 'bytes', { + lt: 8000, + }), + ]); + const actual = fromKueryExpression('bytes > 1000 and bytes < 8000'); + expect(actual).toEqual(expected); + }); + + test('should support inclusive range operators', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('range', 'bytes', { + gte: 1000, + }), + nodeTypes.function.buildNode('range', 'bytes', { + lte: 8000, + }), + ]); + const actual = fromKueryExpression('bytes >= 1000 and bytes <= 8000'); + expect(actual).toEqual(expected); + }); + + test('should support wildcards in field names', () => { + const expected = nodeTypes.function.buildNode('is', 'machine*', 'osx'); + const actual = fromKueryExpression('machine*:osx'); + expect(actual).toEqual(expected); + }); + + test('should support wildcards in values', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'ba*'); + const actual = fromKueryExpression('foo:ba*'); + expect(actual).toEqual(expected); + }); + + test('should create an exists "is" query when a field is given and "*" is the value', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', '*'); + const actual = fromKueryExpression('foo:*'); + expect(actual).toEqual(expected); + }); + + test('should support nested queries indicated by curly braces', () => { + const expected = nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'foo') + ); + const actual = fromKueryExpression('nestedField:{ childOfNested: foo }'); + expect(actual).toEqual(expected); + }); + + test('should support nested subqueries and subqueries inside nested queries', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'response', '200'), + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'childOfNested', 'foo'), + nodeTypes.function.buildNode('is', 'childOfNested', 'bar'), + ]) + ), + ]); + const actual = fromKueryExpression( + 'response:200 and nestedField:{ childOfNested:foo or childOfNested:bar }' + ); + expect(actual).toEqual(expected); + }); + + test('should support nested sub-queries inside paren groups', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'response', '200'), + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'foo') + ), + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'bar') + ), + ]), + ]); + const actual = fromKueryExpression( + 'response:200 and ( nestedField:{ childOfNested:foo } or nestedField:{ childOfNested:bar } )' + ); + expect(actual).toEqual(expected); + }); + + test('should support nested groups inside other nested groups', () => { + const expected = nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode( + 'nested', + 'nestedChild', + nodeTypes.function.buildNode('is', 'doublyNestedChild', 'foo') + ) + ); + const actual = fromKueryExpression('nestedField:{ nestedChild:{ doublyNestedChild:foo } }'); + expect(actual).toEqual(expected); + }); + }); + + describe('fromLiteralExpression', () => { + test('should create literal nodes for unquoted values with correct primitive types', () => { + const stringLiteral = nodeTypes.literal.buildNode('foo'); + const booleanFalseLiteral = nodeTypes.literal.buildNode(false); + const booleanTrueLiteral = nodeTypes.literal.buildNode(true); + const numberLiteral = nodeTypes.literal.buildNode(42); + + expect(fromLiteralExpression('foo')).toEqual(stringLiteral); + expect(fromLiteralExpression('true')).toEqual(booleanTrueLiteral); + expect(fromLiteralExpression('false')).toEqual(booleanFalseLiteral); + expect(fromLiteralExpression('42')).toEqual(numberLiteral); + }); + + test('should allow escaping of special characters with a backslash', () => { + const expected = nodeTypes.literal.buildNode('\\():<>"*'); + // yo dawg + const actual = fromLiteralExpression('\\\\\\(\\)\\:\\<\\>\\"\\*'); + expect(actual).toEqual(expected); + }); + + test('should support double quoted strings that do not need escapes except for quotes', () => { + const expected = nodeTypes.literal.buildNode('\\():<>"*'); + const actual = fromLiteralExpression('"\\():<>\\"*"'); + expect(actual).toEqual(expected); + }); + + test('should support escaped backslashes inside quoted strings', () => { + const expected = nodeTypes.literal.buildNode('\\'); + const actual = fromLiteralExpression('"\\\\"'); + expect(actual).toEqual(expected); + }); + + test('should detect wildcards and build wildcard AST nodes', () => { + const expected = nodeTypes.wildcard.buildNode('foo*bar'); + const actual = fromLiteralExpression('foo*bar'); + expect(actual).toEqual(expected); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return the given node type's ES query representation", () => { + const node = nodeTypes.function.buildNode('exists', 'response'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern); + const result = toElasticsearchQuery(node, indexPattern); + expect(result).toEqual(expected); + }); + + test('should return an empty "and" function for undefined nodes and unknown node types', () => { + const expected = nodeTypes.function.toElasticsearchQuery( + nodeTypes.function.buildNode('and', []), + indexPattern + ); + + expect(toElasticsearchQuery((null as unknown) as KueryNode, undefined)).toEqual(expected); + + const noTypeNode = nodeTypes.function.buildNode('exists', 'foo'); + delete noTypeNode.type; + expect(toElasticsearchQuery(noTypeNode)).toEqual(expected); + + const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo'); + + // @ts-ignore + unknownTypeNode.type = 'notValid'; + expect(toElasticsearchQuery(unknownTypeNode)).toEqual(expected); + }); + + test("should return the given node type's ES query representation including a time zone parameter when one is provided", () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); + const result = toElasticsearchQuery(node, indexPattern, config); + expect(result).toEqual(expected); + }); + }); + + describe('doesKueryExpressionHaveLuceneSyntaxError', () => { + test('should return true for Lucene ranges', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]'); + expect(result).toEqual(true); + }); + + test('should return false for KQL ranges', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar < 1'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene exists', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar'); + expect(result).toEqual(true); + }); + + test('should return false for KQL exists', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar:*'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene wildcards', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba?'); + expect(result).toEqual(true); + }); + + test('should return false for KQL wildcards', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba*'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene regex', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene fuzziness', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba~'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene proximity', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene boosting', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene + operator', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('+foo: bar'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene - operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('-foo: bar'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene && operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene || operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux'); + expect(result).toEqual(true); + }); + + test('should return true for mixed KQL/Lucene queries', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)'); + expect(result).toEqual(true); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/ast/ast.js b/src/plugins/data/common/es_query/kuery/ast/ast.ts similarity index 53% rename from packages/kbn-es-query/src/kuery/ast/ast.js rename to src/plugins/data/common/es_query/kuery/ast/ast.ts index 1688995d46f80..253f432617972 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.js +++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts @@ -17,21 +17,44 @@ * under the License. */ -import _ from 'lodash'; import { nodeTypes } from '../node_types/index'; -import { parse as parseKuery } from './kuery'; -import { KQLSyntaxError } from '../errors'; +import { KQLSyntaxError } from '../kuery_syntax_error'; +import { KueryNode, JsonObject, DslQuery, KueryParseOptions } from '../types'; +import { IIndexPattern } from '../../../index_patterns/types'; -export function fromLiteralExpression(expression, parseOptions) { - parseOptions = { - ...parseOptions, - startRule: 'Literal', - }; +// @ts-ignore +import { parse as parseKuery } from './_generated_/kuery'; - return fromExpression(expression, parseOptions, parseKuery); -} +const fromExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {}, + parse: Function = parseKuery +): KueryNode => { + if (typeof expression === 'undefined') { + throw new Error('expression must be a string, got undefined instead'); + } + + return parse(expression, { ...parseOptions, helpers: { nodeTypes } }); +}; + +export const fromLiteralExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {} +): KueryNode => { + return fromExpression( + expression, + { + ...parseOptions, + startRule: 'Literal', + }, + parseKuery + ); +}; -export function fromKueryExpression(expression, parseOptions) { +export const fromKueryExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {} +): KueryNode => { try { return fromExpression(expression, parseOptions, parseKuery); } catch (error) { @@ -41,20 +64,18 @@ export function fromKueryExpression(expression, parseOptions) { throw error; } } -} +}; -function fromExpression(expression, parseOptions = {}, parse = parseKuery) { - if (_.isUndefined(expression)) { - throw new Error('expression must be a string, got undefined instead'); +export const doesKueryExpressionHaveLuceneSyntaxError = ( + expression: string | DslQuery +): boolean => { + try { + fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery); + return false; + } catch (e) { + return e.message.startsWith('Lucene'); } - - parseOptions = { - ...parseOptions, - helpers: { nodeTypes }, - }; - - return parse(expression, parseOptions); -} +}; /** * @params {String} indexPattern @@ -63,19 +84,17 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) { * IndexPattern isn't required, but if you pass one in, we can be more intelligent * about how we craft the queries (e.g. scripted fields) */ -export function toElasticsearchQuery(node, indexPattern, config = {}, context = {}) { +export const toElasticsearchQuery = ( + node: KueryNode, + indexPattern?: IIndexPattern, + config?: Record, + context?: Record +): JsonObject => { if (!node || !node.type || !nodeTypes[node.type]) { - return toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); + return toElasticsearchQuery(nodeTypes.function.buildNode('and', []), indexPattern); } - return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern, config, context); -} + const nodeType = (nodeTypes[node.type] as unknown) as any; -export function doesKueryExpressionHaveLuceneSyntaxError(expression) { - try { - fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery); - return false; - } catch (e) { - return (e.message.startsWith('Lucene')); - } -} + return nodeType.toElasticsearchQuery(node, indexPattern, config, context); +}; diff --git a/packages/kbn-es-query/src/kuery/ast/index.js b/src/plugins/data/common/es_query/kuery/ast/index.ts similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/index.js rename to src/plugins/data/common/es_query/kuery/ast/index.ts diff --git a/packages/kbn-es-query/src/kuery/ast/kuery.peg b/src/plugins/data/common/es_query/kuery/ast/kuery.peg similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/kuery.peg rename to src/plugins/data/common/es_query/kuery/ast/kuery.peg diff --git a/packages/kbn-es-query/src/kuery/functions/and.js b/src/plugins/data/common/es_query/kuery/functions/and.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/and.js rename to src/plugins/data/common/es_query/kuery/functions/and.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js b/src/plugins/data/common/es_query/kuery/functions/and.test.ts similarity index 50% rename from packages/kbn-es-query/src/kuery/functions/__tests__/and.js rename to src/plugins/data/common/es_query/kuery/functions/and.test.ts index 07289a878e8c1..133e691b27dba 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js +++ b/src/plugins/data/common/es_query/kuery/functions/and.test.ts @@ -17,43 +17,53 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as and from '../and'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import * as ast from '../ast'; -let indexPattern; +// @ts-ignore +import * as and from './and'; const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); -describe('kuery functions', function () { - describe('and', function () { +describe('kuery functions', () => { + describe('and', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = and.buildNodeParams([childNode1, childNode2]); - const { arguments: [ actualChildNode1, actualChildNode2 ] } = result; - expect(actualChildNode1).to.be(childNode1); - expect(actualChildNode2).to.be(childNode2); + const { + arguments: [actualChildNode1, actualChildNode2], + } = result; + + expect(actualChildNode1).toBe(childNode1); + expect(actualChildNode2).toBe(childNode2); }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES bool query\'s filter clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap subqueries in an ES bool query's filter clause", () => { const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]); const result = and.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.only.have.keys('filter'); - expect(result.bool.filter).to.eql( - [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + expect(result.bool).toHaveProperty('filter'); + expect(Object.keys(result.bool).length).toBe(1); + + expect(result.bool.filter).toEqual( + [childNode1, childNode2].map(childNode => + ast.toElasticsearchQuery(childNode, indexPattern) + ) ); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/exists.js b/src/plugins/data/common/es_query/kuery/functions/exists.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/exists.js rename to src/plugins/data/common/es_query/kuery/functions/exists.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js b/src/plugins/data/common/es_query/kuery/functions/exists.test.ts similarity index 51% rename from packages/kbn-es-query/src/kuery/functions/__tests__/exists.js rename to src/plugins/data/common/es_query/kuery/functions/exists.test.ts index ee4cfab94e614..8443436cf4cfb 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js +++ b/src/plugins/data/common/es_query/kuery/functions/exists.test.ts @@ -17,67 +17,73 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as exists from '../exists'; -import { nodeTypes } from '../../node_types'; -import _ from 'lodash'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +// @ts-ignore +import * as exists from './exists'; -let indexPattern; - -describe('kuery functions', function () { - describe('exists', function () { +describe('kuery functions', () => { + describe('exists', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - it('should return a single "arguments" param', function () { + describe('buildNodeParams', () => { + test('should return a single "arguments" param', () => { const result = exists.buildNodeParams('response'); - expect(result).to.only.have.key('arguments'); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); }); - it('arguments should contain the provided fieldName as a literal', function () { - const { arguments: [ arg ] } = exists.buildNodeParams('response'); - expect(arg).to.have.property('type', 'literal'); - expect(arg).to.have.property('value', 'response'); + test('arguments should contain the provided fieldName as a literal', () => { + const { + arguments: [arg], + } = exists.buildNodeParams('response'); + + expect(arg).toHaveProperty('type', 'literal'); + expect(arg).toHaveProperty('value', 'response'); }); }); - describe('toElasticsearchQuery', function () { - it('should return an ES exists query', function () { + describe('toElasticsearchQuery', () => { + test('should return an ES exists query', () => { const expected = { - exists: { field: 'response' } + exists: { field: 'response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery(existsNode, indexPattern); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); - it('should return an ES exists query without an index pattern', function () { + test('should return an ES exists query without an index pattern', () => { const expected = { - exists: { field: 'response' } + exists: { field: 'response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery(existsNode); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); - it('should throw an error for scripted fields', function () { + test('should throw an error for scripted fields', () => { const existsNode = nodeTypes.function.buildNode('exists', 'script string'); - expect(exists.toElasticsearchQuery) - .withArgs(existsNode, indexPattern).to.throwException(/Exists query does not support scripted fields/); + expect(() => exists.toElasticsearchQuery(existsNode, indexPattern)).toThrowError( + /Exists query does not support scripted fields/ + ); }); - it('should use a provided nested context to create a full field name', function () { + test('should use a provided nested context to create a full field name', () => { const expected = { - exists: { field: 'nestedField.response' } + exists: { field: 'nestedField.response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery( existsNode, @@ -85,7 +91,8 @@ describe('kuery functions', function () { {}, { nested: { path: 'nestedField' } } ); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.js b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/geo_bounding_box.js rename to src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.js diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts new file mode 100644 index 0000000000000..cf287ff2c437a --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from 'lodash'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as geoBoundingBox from './geo_bounding_box'; + +const params = { + bottomRight: { + lat: 50.73, + lon: -135.35, + }, + topLeft: { + lat: 73.12, + lon: -174.37, + }, +}; + +describe('kuery functions', () => { + describe('geoBoundingBox', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return an "arguments" param', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'geo'); + }); + + test('arguments should contain the provided params as named arguments with "lat, lon" string values', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + const { + arguments: [, ...args], + } = result; + + args.map((param: any) => { + expect(param).toHaveProperty('type', 'namedArg'); + expect(['bottomRight', 'topLeft'].includes(param.name)).toBe(true); + expect(param.value.type).toBe('literal'); + + const { lat, lon } = get(params, param.name); + + expect(param.value.value).toBe(`${lat}, ${lon}`); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES geo_bounding_box query representing the given node', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box.geo).toHaveProperty('top_left', '73.12, -174.37'); + expect(result.geo_bounding_box.geo).toHaveProperty('bottom_right', '50.73, -135.35'); + }); + + test('should return an ES geo_bounding_box query without an index pattern', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box.geo).toHaveProperty('top_left', '73.12, -174.37'); + expect(result.geo_bounding_box.geo).toHaveProperty('bottom_right', '50.73, -135.35'); + }); + + test('should use the ignore_unmapped parameter', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + + expect(result.geo_bounding_box.ignore_unmapped).toBe(true); + }); + + test('should throw an error for scripted fields', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params); + + expect(() => geoBoundingBox.toElasticsearchQuery(node, indexPattern)).toThrowError( + /Geo bounding box query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box['nestedField.geo']).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_polygon.js b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/geo_polygon.js rename to src/plugins/data/common/es_query/kuery/functions/geo_polygon.js diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts new file mode 100644 index 0000000000000..84500cb4ade7e --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as geoPolygon from './geo_polygon'; + +const points = [ + { + lat: 69.77, + lon: -171.56, + }, + { + lat: 50.06, + lon: -169.1, + }, + { + lat: 69.16, + lon: -125.85, + }, +]; + +describe('kuery functions', () => { + describe('geoPolygon', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return an "arguments" param', () => { + const result = geoPolygon.buildNodeParams('geo', points); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const result = geoPolygon.buildNodeParams('geo', points); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'geo'); + }); + + test('arguments should contain the provided points literal "lat, lon" string values', () => { + const result = geoPolygon.buildNodeParams('geo', points); + const { + arguments: [, ...args], + } = result; + + args.forEach((param: any, index: number) => { + const expectedPoint = points[index]; + const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`; + + expect(param).toHaveProperty('type', 'literal'); + expect(param.value).toBe(expectedLatLon); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES geo_polygon query representing the given node', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon.geo).toHaveProperty('points'); + + result.geo_polygon.geo.points.forEach((point: any, index: number) => { + const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; + + expect(point).toBe(expectedLatLon); + }); + }); + + test('should return an ES geo_polygon query without an index pattern', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon.geo).toHaveProperty('points'); + + result.geo_polygon.geo.points.forEach((point: any, index: number) => { + const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; + + expect(point).toBe(expectedLatLon); + }); + }); + + test('should use the ignore_unmapped parameter', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node, indexPattern); + + expect(result.geo_polygon.ignore_unmapped).toBe(true); + }); + + test('should throw an error for scripted fields', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points); + expect(() => geoPolygon.toElasticsearchQuery(node, indexPattern)).toThrowError( + /Geo polygon query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon['nestedField.geo']).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/index.js b/src/plugins/data/common/es_query/kuery/functions/index.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/index.js rename to src/plugins/data/common/es_query/kuery/functions/index.js diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/src/plugins/data/common/es_query/kuery/functions/is.js similarity index 95% rename from packages/kbn-es-query/src/kuery/functions/is.js rename to src/plugins/data/common/es_query/kuery/functions/is.js index 63ade9e8793a7..4f2f298c4707d 100644 --- a/packages/kbn-es-query/src/kuery/functions/is.js +++ b/src/plugins/data/common/es_query/kuery/functions/is.js @@ -17,20 +17,22 @@ * under the License. */ -import _ from 'lodash'; -import * as ast from '../ast'; -import * as literal from '../node_types/literal'; -import * as wildcard from '../node_types/wildcard'; -import { getPhraseScript } from '../../utils/filters'; +import { get, isUndefined } from 'lodash'; +import { getPhraseScript } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; +import * as ast from '../ast'; + +import * as literal from '../node_types/literal'; +import * as wildcard from '../node_types/wildcard'; + export function buildNodeParams(fieldName, value, isPhrase = false) { - if (_.isUndefined(fieldName)) { + if (isUndefined(fieldName)) { throw new Error('fieldName is a required argument'); } - if (_.isUndefined(value)) { + if (isUndefined(value)) { throw new Error('value is a required argument'); } const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName); @@ -45,7 +47,7 @@ export function toElasticsearchQuery(node, indexPattern = null, config = {}, con const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node; const fullFieldNameArg = getFullFieldNameNode(fieldNameArg, indexPattern, context.nested ? context.nested.path : undefined); const fieldName = ast.toElasticsearchQuery(fullFieldNameArg); - const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; + const value = !isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; const type = isPhraseArg.value ? 'phrase' : 'best_fields'; if (fullFieldNameArg.value === null) { if (valueArg.type === 'wildcard') { @@ -94,7 +96,7 @@ export function toElasticsearchQuery(node, indexPattern = null, config = {}, con // users handle this themselves so we automatically add nested queries in this scenario. if ( !(fullFieldNameArg.type === 'wildcard') - || !_.get(field, 'subType.nested') + || !get(field, 'subType.nested') || context.nested ) { return query; diff --git a/src/plugins/data/common/es_query/kuery/functions/is.test.ts b/src/plugins/data/common/es_query/kuery/functions/is.test.ts new file mode 100644 index 0000000000000..df147bad54a34 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/is.test.ts @@ -0,0 +1,305 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; + +// @ts-ignore +import * as is from './is'; +import { IIndexPattern } from '../../../index_patterns'; + +describe('kuery functions', () => { + describe('is', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('fieldName and value should be required arguments', () => { + expect(() => is.buildNodeParams()).toThrowError(/fieldName is a required argument/); + expect(() => is.buildNodeParams('foo')).toThrowError(/value is a required argument/); + }); + + test('arguments should contain the provided fieldName and value as literals', () => { + const { + arguments: [fieldName, value], + } = is.buildNodeParams('response', 200); + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'response'); + expect(value).toHaveProperty('type', 'literal'); + expect(value).toHaveProperty('value', 200); + }); + + test('should detect wildcards in the provided arguments', () => { + const { + arguments: [fieldName, value], + } = is.buildNodeParams('machine*', 'win*'); + + expect(fieldName).toHaveProperty('type', 'wildcard'); + expect(value).toHaveProperty('type', 'wildcard'); + }); + + test('should default to a non-phrase query', () => { + const { + arguments: [, , isPhrase], + } = is.buildNodeParams('response', 200); + expect(isPhrase.value).toBe(false); + }); + + test('should allow specification of a phrase query', () => { + const { + arguments: [, , isPhrase], + } = is.buildNodeParams('response', 200, true); + expect(isPhrase.value).toBe(true); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES match_all query when fieldName and value are both "*"', () => { + const expected = { + match_all: {}, + }; + const node = nodeTypes.function.buildNode('is', '*', '*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES multi_match query using default_field when fieldName is null', () => { + const expected = { + multi_match: { + query: 200, + type: 'best_fields', + lenient: true, + }, + }; + const node = nodeTypes.function.buildNode('is', null, 200); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES query_string query using default_field when fieldName is null and value contains a wildcard', () => { + const expected = { + query_string: { + query: 'jpg*', + }, + }; + const node = nodeTypes.function.buildNode('is', null, 'jpg*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES bool query with a sub-query for each field when fieldName is "*"', () => { + const node = nodeTypes.function.buildNode('is', '*', 200); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('bool'); + expect(result.bool.should.length).toBe(indexPattern.fields.length); + }); + + test('should return an ES exists query when value is "*"', () => { + const expected = { + bool: { + should: [{ exists: { field: 'extension' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', '*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES match query when a concrete fieldName and value are provided', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES match query when a concrete fieldName and value are provided without an index pattern', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery(node); + + expect(result).toEqual(expected); + }); + + test('should support creation of phrase queries', () => { + const expected = { + bool: { + should: [{ match_phrase: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg', true); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should create a query_string query for wildcard values', () => { + const expected = { + bool: { + should: [ + { + query_string: { + fields: ['extension'], + query: 'jpg*', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support scripted fields', () => { + const node = nodeTypes.function.buildNode('is', 'script string', 'foo'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result.bool.should[0]).toHaveProperty('script'); + }); + + test('should support date fields without a dateFormat provided', () => { + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support date fields with a dateFormat provided', () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + time_zone: 'America/Phoenix', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern, config); + + expect(result).toEqual(expected); + }); + + test('should use a provided nested context to create a full field name', () => { + const expected = { + bool: { + should: [{ match: { 'nestedField.extension': 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toEqual(expected); + }); + + test('should support wildcard field names', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should automatically add a nested query when a wildcard field name covers a nested field', () => { + const expected = { + bool: { + should: [ + { + nested: { + path: 'nestedField.nestedChild', + query: { + match: { + 'nestedField.nestedChild.doublyNestedChild': 'foo', + }, + }, + score_mode: 'none', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/nested.js b/src/plugins/data/common/es_query/kuery/functions/nested.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/nested.js rename to src/plugins/data/common/es_query/kuery/functions/nested.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js b/src/plugins/data/common/es_query/kuery/functions/nested.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/functions/__tests__/nested.js rename to src/plugins/data/common/es_query/kuery/functions/nested.test.ts index 5ba73e485ddf1..945a36d304a05 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js +++ b/src/plugins/data/common/es_query/kuery/functions/nested.test.ts @@ -17,52 +17,60 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as nested from '../nested'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; + +// @ts-ignore +import * as nested from './nested'; const childNode = nodeTypes.function.buildNode('is', 'child', 'foo'); -describe('kuery functions', function () { - describe('nested', function () { +describe('kuery functions', () => { + describe('nested', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = nested.buildNodeParams('nestedField', childNode); - const { arguments: [ resultPath, resultChildNode ] } = result; - expect(ast.toElasticsearchQuery(resultPath)).to.be('nestedField'); - expect(resultChildNode).to.be(childNode); + const { + arguments: [resultPath, resultChildNode], + } = result; + + expect(ast.toElasticsearchQuery(resultPath)).toBe('nestedField'); + expect(resultChildNode).toBe(childNode); }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES nested query', function () { + describe('toElasticsearchQuery', () => { + test('should wrap subqueries in an ES nested query', () => { const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); const result = nested.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('nested'); - expect(result.nested.path).to.be('nestedField'); - expect(result.nested.score_mode).to.be('none'); + + expect(result).toHaveProperty('nested'); + expect(Object.keys(result).length).toBe(1); + + expect(result.nested.path).toBe('nestedField'); + expect(result.nested.score_mode).toBe('none'); }); - it('should pass the nested path to subqueries so the full field name can be used', function () { + test('should pass the nested path to subqueries so the full field name can be used', () => { const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); const result = nested.toElasticsearchQuery(node, indexPattern); const expectedSubQuery = ast.toElasticsearchQuery( nodeTypes.function.buildNode('is', 'nestedField.child', 'foo') ); - expect(result.nested.query).to.eql(expectedSubQuery); - }); + expect(result.nested.query).toEqual(expectedSubQuery); + }); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/not.js b/src/plugins/data/common/es_query/kuery/functions/not.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/not.js rename to src/plugins/data/common/es_query/kuery/functions/not.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js b/src/plugins/data/common/es_query/kuery/functions/not.test.ts similarity index 50% rename from packages/kbn-es-query/src/kuery/functions/__tests__/not.js rename to src/plugins/data/common/es_query/kuery/functions/not.test.ts index 7a2d7fa39c152..01c1976b939ea 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js +++ b/src/plugins/data/common/es_query/kuery/functions/not.test.ts @@ -17,44 +17,50 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as not from '../not'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; -const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); +// @ts-ignore +import * as not from './not'; -describe('kuery functions', function () { +const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - describe('not', function () { +describe('kuery functions', () => { + describe('not', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child node', () => { + const { + arguments: [actualChild], + } = not.buildNodeParams(childNode); - it('arguments should contain the unmodified child node', function () { - const { arguments: [ actualChild ] } = not.buildNodeParams(childNode); - expect(actualChild).to.be(childNode); + expect(actualChild).toBe(childNode); }); - - }); - describe('toElasticsearchQuery', function () { - - it('should wrap a subquery in an ES bool query\'s must_not clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap a subquery in an ES bool query's must_not clause", () => { const node = nodeTypes.function.buildNode('not', childNode); const result = not.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.only.have.keys('must_not'); - expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern)); - }); + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + + expect(result.bool).toHaveProperty('must_not'); + expect(Object.keys(result.bool).length).toBe(1); + + expect(result.bool.must_not).toEqual(ast.toElasticsearchQuery(childNode, indexPattern)); + }); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/or.js b/src/plugins/data/common/es_query/kuery/functions/or.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/or.js rename to src/plugins/data/common/es_query/kuery/functions/or.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js b/src/plugins/data/common/es_query/kuery/functions/or.test.ts similarity index 52% rename from packages/kbn-es-query/src/kuery/functions/__tests__/or.js rename to src/plugins/data/common/es_query/kuery/functions/or.test.ts index f24f24b98e7fb..a6590546e5fc5 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js +++ b/src/plugins/data/common/es_query/kuery/functions/or.test.ts @@ -17,56 +17,61 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as or from '../or'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; + +// @ts-ignore +import * as or from './or'; const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); -describe('kuery functions', function () { - - describe('or', function () { +describe('kuery functions', () => { + describe('or', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = or.buildNodeParams([childNode1, childNode2]); - const { arguments: [ actualChildNode1, actualChildNode2 ] } = result; - expect(actualChildNode1).to.be(childNode1); - expect(actualChildNode2).to.be(childNode2); - }); + const { + arguments: [actualChildNode1, actualChildNode2], + } = result; + expect(actualChildNode1).toBe(childNode1); + expect(actualChildNode2).toBe(childNode2); + }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES bool query\'s should clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap subqueries in an ES bool query's should clause", () => { const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); const result = or.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.have.keys('should'); - expect(result.bool.should).to.eql( - [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + expect(result.bool).toHaveProperty('should'); + expect(result.bool.should).toEqual( + [childNode1, childNode2].map(childNode => + ast.toElasticsearchQuery(childNode, indexPattern) + ) ); }); - it('should require one of the clauses to match', function () { + test('should require one of the clauses to match', () => { const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); const result = or.toElasticsearchQuery(node, indexPattern); - expect(result.bool).to.have.property('minimum_should_match', 1); - }); + expect(result.bool).toHaveProperty('minimum_should_match', 1); + }); }); - }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/range.js b/src/plugins/data/common/es_query/kuery/functions/range.js similarity index 98% rename from packages/kbn-es-query/src/kuery/functions/range.js rename to src/plugins/data/common/es_query/kuery/functions/range.js index f7719998ad524..80181cfc003f1 100644 --- a/packages/kbn-es-query/src/kuery/functions/range.js +++ b/src/plugins/data/common/es_query/kuery/functions/range.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { nodeTypes } from '../node_types'; import * as ast from '../ast'; -import { getRangeScript } from '../../utils/filters'; +import { getRangeScript } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js b/src/plugins/data/common/es_query/kuery/functions/range.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/functions/__tests__/range.js rename to src/plugins/data/common/es_query/kuery/functions/range.test.ts index 2361e8bb66769..ed8e40830df02 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js +++ b/src/plugins/data/common/es_query/kuery/functions/range.test.ts @@ -17,53 +17,57 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as range from '../range'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { get } from 'lodash'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import { RangeFilterParams } from '../../filters'; -let indexPattern; - -describe('kuery functions', function () { - - describe('range', function () { +// @ts-ignore +import * as range from './range'; +describe('kuery functions', () => { + describe('range', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the provided fieldName as a literal', function () { + describe('buildNodeParams', () => { + test('arguments should contain the provided fieldName as a literal', () => { const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 }); - const { arguments: [fieldName] } = result; + const { + arguments: [fieldName], + } = result; - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'bytes'); + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'bytes'); }); - it('arguments should contain the provided params as named arguments', function () { - const givenParams = { gt: 1000, lt: 8000, format: 'epoch_millis' }; + test('arguments should contain the provided params as named arguments', () => { + const givenParams: RangeFilterParams = { gt: 1000, lt: 8000, format: 'epoch_millis' }; const result = range.buildNodeParams('bytes', givenParams); - const { arguments: [, ...params] } = result; + const { + arguments: [, ...params], + } = result; - expect(params).to.be.an('array'); - expect(params).to.not.be.empty(); + expect(Array.isArray(params)).toBeTruthy(); + expect(params.length).toBeGreaterThan(1); - params.map((param) => { - expect(param).to.have.property('type', 'namedArg'); - expect(['gt', 'lt', 'format'].includes(param.name)).to.be(true); - expect(param.value.type).to.be('literal'); - expect(param.value.value).to.be(givenParams[param.name]); + params.map((param: any) => { + expect(param).toHaveProperty('type', 'namedArg'); + expect(['gt', 'lt', 'format'].includes(param.name)).toBe(true); + expect(param.value.type).toBe('literal'); + expect(param.value.value).toBe(get(givenParams, param.name)); }); }); - }); - describe('toElasticsearchQuery', function () { - - it('should return an ES range query for the node\'s field and params', function () { + describe('toElasticsearchQuery', () => { + test("should return an ES range query for the node's field and params", () => { const expected = { bool: { should: [ @@ -71,21 +75,21 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should return an ES range query without an index pattern', function () { + test('should return an ES range query without an index pattern', () => { const expected = { bool: { should: [ @@ -93,21 +97,22 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support wildcard field names', function () { + test('should support wildcard field names', () => { const expected = { bool: { should: [ @@ -115,27 +120,29 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; const node = nodeTypes.function.buildNode('range', 'byt*', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support scripted fields', function () { + test('should support scripted fields', () => { const node = nodeTypes.function.buildNode('range', 'script number', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result.bool.should[0]).to.have.key('script'); + + expect(result.bool.should[0]).toHaveProperty('script'); }); - it('should support date fields without a dateFormat provided', function () { + test('should support date fields without a dateFormat provided', () => { const expected = { bool: { should: [ @@ -144,20 +151,23 @@ describe('kuery functions', function () { '@timestamp': { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17', - } - } - } + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); + const node = nodeTypes.function.buildNode('range', '@timestamp', { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support date fields with a dateFormat provided', function () { + test('should support date fields with a dateFormat provided', () => { const config = { dateFormatTZ: 'America/Phoenix' }; const expected = { bool: { @@ -168,20 +178,23 @@ describe('kuery functions', function () { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17', time_zone: 'America/Phoenix', - } - } - } + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); + const node = nodeTypes.function.buildNode('range', '@timestamp', { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }); const result = range.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should use a provided nested context to create a full field name', function () { + test('should use a provided nested context to create a full field name', () => { const expected = { bool: { should: [ @@ -189,15 +202,14 @@ describe('kuery functions', function () { range: { 'nestedField.bytes': { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery( node, @@ -205,10 +217,11 @@ describe('kuery functions', function () { {}, { nested: { path: 'nestedField' } } ); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should automatically add a nested query when a wildcard field name covers a nested field', function () { + test('should automatically add a nested query when a wildcard field name covers a nested field', () => { const expected = { bool: { should: [ @@ -219,21 +232,24 @@ describe('kuery functions', function () { range: { 'nestedField.nestedChild.doublyNestedChild': { gt: 1000, - lt: 8000 - } - } + lt: 8000, + }, + }, }, - score_mode: 'none' - } - } + score_mode: 'none', + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '*doublyNested*', { gt: 1000, lt: 8000 }); + const node = nodeTypes.function.buildNode('range', '*doublyNested*', { + gt: 1000, + lt: 8000, + }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_fields.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/utils/get_fields.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_fields.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts similarity index 52% rename from packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts index 7718479130a8a..d48f0943082c9 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts @@ -17,39 +17,41 @@ * under the License. */ -import { getFields } from '../../utils/get_fields'; -import expect from '@kbn/expect'; -import indexPatternResponse from '../../../../__fixtures__/index_pattern_response.json'; +import { fields } from '../../../../index_patterns/mocks'; -import { nodeTypes } from '../../..'; +import { nodeTypes } from '../../index'; +import { IIndexPattern, IFieldType } from '../../../../index_patterns'; -let indexPattern; - -describe('getFields', function () { +// @ts-ignore +import { getFields } from './get_fields'; +describe('getFields', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('field names without a wildcard', function () { - - it('should return an empty array if the field does not exist in the index pattern', function () { + describe('field names without a wildcard', () => { + test('should return an empty array if the field does not exist in the index pattern', () => { const fieldNameNode = nodeTypes.literal.buildNode('nonExistentField'); - const expected = []; const actual = getFields(fieldNameNode, indexPattern); - expect(actual).to.eql(expected); + + expect(actual).toEqual([]); }); - it('should return the single matching field in an array', function () { + test('should return the single matching field in an array', () => { const fieldNameNode = nodeTypes.literal.buildNode('extension'); const results = getFields(fieldNameNode, indexPattern); - expect(results).to.be.an('array'); - expect(results).to.have.length(1); - expect(results[0].name).to.be('extension'); + + expect(results).toHaveLength(1); + expect(Array.isArray(results)).toBeTruthy(); + expect(results[0].name).toBe('extension'); }); - it('should not match a wildcard in a literal node', function () { + test('should not match a wildcard in a literal node', () => { const indexPatternWithWildField = { title: 'wildIndex', fields: [ @@ -61,37 +63,32 @@ describe('getFields', function () { const fieldNameNode = nodeTypes.literal.buildNode('foo*'); const results = getFields(fieldNameNode, indexPatternWithWildField); - expect(results).to.be.an('array'); - expect(results).to.have.length(1); - expect(results[0].name).to.be('foo*'); - // ensure the wildcard is not actually being parsed - const expected = []; + expect(results).toHaveLength(1); + expect(Array.isArray(results)).toBeTruthy(); + expect(results[0].name).toBe('foo*'); + const actual = getFields(nodeTypes.literal.buildNode('fo*'), indexPatternWithWildField); - expect(actual).to.eql(expected); + expect(actual).toEqual([]); }); }); - describe('field name patterns with a wildcard', function () { - - it('should return an empty array if it does not match any fields in the index pattern', function () { + describe('field name patterns with a wildcard', () => { + test('should return an empty array if test does not match any fields in the index pattern', () => { const fieldNameNode = nodeTypes.wildcard.buildNode('nonExistent*'); - const expected = []; const actual = getFields(fieldNameNode, indexPattern); - expect(actual).to.eql(expected); + + expect(actual).toEqual([]); }); - it('should return all fields that match the pattern in an array', function () { + test('should return all fields that match the pattern in an array', () => { const fieldNameNode = nodeTypes.wildcard.buildNode('machine*'); const results = getFields(fieldNameNode, indexPattern); - expect(results).to.be.an('array'); - expect(results).to.have.length(2); - expect(results.find((field) => { - return field.name === 'machine.os'; - })).to.be.ok(); - expect(results.find((field) => { - return field.name === 'machine.os.raw'; - })).to.be.ok(); + + expect(Array.isArray(results)).toBeTruthy(); + expect(results).toHaveLength(2); + expect(results.find((field: IFieldType) => field.name === 'machine.os')).toBeDefined(); + expect(results.find((field: IFieldType) => field.name === 'machine.os.raw')).toBeDefined(); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.js diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts new file mode 100644 index 0000000000000..e138e22b76ad3 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { nodeTypes } from '../../node_types'; +import { fields } from '../../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../../index_patterns'; + +// @ts-ignore +import { getFullFieldNameNode } from './get_full_field_name_node'; + +describe('getFullFieldNameNode', function() { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + test('should return unchanged name node if no nested path is passed in', () => { + const nameNode = nodeTypes.literal.buildNode('notNested'); + const result = getFullFieldNameNode(nameNode, indexPattern); + + expect(result).toEqual(nameNode); + }); + + test('should add the nested path if test is valid according to the index pattern', () => { + const nameNode = nodeTypes.literal.buildNode('child'); + const result = getFullFieldNameNode(nameNode, indexPattern, 'nestedField'); + + expect(result).toEqual(nodeTypes.literal.buildNode('nestedField.child')); + }); + + test('should throw an error if a path is provided for a non-nested field', () => { + const nameNode = nodeTypes.literal.buildNode('os'); + expect(() => getFullFieldNameNode(nameNode, indexPattern, 'machine')).toThrowError( + /machine.os is not a nested field but is in nested group "machine" in the KQL expression/ + ); + }); + + test('should throw an error if a nested field is not passed with a path', () => { + const nameNode = nodeTypes.literal.buildNode('nestedField.child'); + + expect(() => getFullFieldNameNode(nameNode, indexPattern)).toThrowError( + /nestedField.child is a nested field, but is not in a nested group in the KQL expression./ + ); + }); + + test('should throw an error if a nested field is passed with the wrong path', () => { + const nameNode = nodeTypes.literal.buildNode('nestedChild.doublyNestedChild'); + + expect(() => getFullFieldNameNode(nameNode, indexPattern, 'nestedField')).toThrowError( + /Nested field nestedField.nestedChild.doublyNestedChild is being queried with the incorrect nested path. The correct path is nestedField.nestedChild/ + ); + }); + + test('should skip error checking for wildcard names', () => { + const nameNode = nodeTypes.wildcard.buildNode('nested*'); + const result = getFullFieldNameNode(nameNode, indexPattern); + + expect(result).toEqual(nameNode); + }); + + test('should skip error checking if no index pattern is passed in', () => { + const nameNode = nodeTypes.literal.buildNode('os'); + expect(() => getFullFieldNameNode(nameNode, null, 'machine')).not.toThrowError(); + + const result = getFullFieldNameNode(nameNode, null, 'machine'); + expect(result).toEqual(nodeTypes.literal.buildNode('machine.os')); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/index.js b/src/plugins/data/common/es_query/kuery/index.ts similarity index 91% rename from packages/kbn-es-query/src/kuery/index.js rename to src/plugins/data/common/es_query/kuery/index.ts index e0cacada7f274..4184dea62ef2c 100644 --- a/packages/kbn-es-query/src/kuery/index.js +++ b/src/plugins/data/common/es_query/kuery/index.ts @@ -17,6 +17,8 @@ * under the License. */ -export * from './ast'; +export { KQLSyntaxError } from './kuery_syntax_error'; export { nodeTypes } from './node_types'; -export * from './errors'; +export * from './ast'; + +export * from './types'; diff --git a/packages/kbn-es-query/src/kuery/errors/index.test.js b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts similarity index 66% rename from packages/kbn-es-query/src/kuery/errors/index.test.js rename to src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts index d8040e464b696..cfe2f86e813ca 100644 --- a/packages/kbn-es-query/src/kuery/errors/index.test.js +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts @@ -17,89 +17,92 @@ * under the License. */ -import { fromKueryExpression } from '../ast'; - +import { fromKueryExpression } from './ast'; describe('kql syntax errors', () => { - it('should throw an error for a field query missing a value', () => { expect(() => { fromKueryExpression('response:'); - }).toThrow('Expected "(", "{", value, whitespace but end of input found.\n' + - 'response:\n' + - '---------^'); + }).toThrow( + 'Expected "(", "{", value, whitespace but end of input found.\n' + + 'response:\n' + + '---------^' + ); }); it('should throw an error for an OR query missing a right side sub-query', () => { expect(() => { fromKueryExpression('response:200 or '); - }).toThrow('Expected "(", NOT, field name, value but end of input found.\n' + - 'response:200 or \n' + - '----------------^'); + }).toThrow( + 'Expected "(", NOT, field name, value but end of input found.\n' + + 'response:200 or \n' + + '----------------^' + ); }); it('should throw an error for an OR list of values missing a right side sub-query', () => { expect(() => { fromKueryExpression('response:(200 or )'); - }).toThrow('Expected "(", NOT, value but ")" found.\n' + - 'response:(200 or )\n' + - '-----------------^'); + }).toThrow( + 'Expected "(", NOT, value but ")" found.\n' + 'response:(200 or )\n' + '-----------------^' + ); }); it('should throw an error for a NOT query missing a sub-query', () => { expect(() => { fromKueryExpression('response:200 and not '); - }).toThrow('Expected "(", field name, value but end of input found.\n' + - 'response:200 and not \n' + - '---------------------^'); + }).toThrow( + 'Expected "(", field name, value but end of input found.\n' + + 'response:200 and not \n' + + '---------------------^' + ); }); it('should throw an error for a NOT list missing a sub-query', () => { expect(() => { fromKueryExpression('response:(200 and not )'); - }).toThrow('Expected "(", value but ")" found.\n' + - 'response:(200 and not )\n' + - '----------------------^'); + }).toThrow( + 'Expected "(", value but ")" found.\n' + + 'response:(200 and not )\n' + + '----------------------^' + ); }); it('should throw an error for unbalanced quotes', () => { expect(() => { fromKueryExpression('foo:"ba '); - }).toThrow('Expected "(", "{", value, whitespace but """ found.\n' + - 'foo:"ba \n' + - '----^'); + }).toThrow('Expected "(", "{", value, whitespace but """ found.\n' + 'foo:"ba \n' + '----^'); }); it('should throw an error for unescaped quotes in a quoted string', () => { expect(() => { fromKueryExpression('foo:"ba "r"'); - }).toThrow('Expected AND, OR, end of input, whitespace but "r" found.\n' + - 'foo:"ba "r"\n' + - '---------^'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but "r" found.\n' + 'foo:"ba "r"\n' + '---------^' + ); }); it('should throw an error for unescaped special characters in literals', () => { expect(() => { fromKueryExpression('foo:ba:r'); - }).toThrow('Expected AND, OR, end of input, whitespace but ":" found.\n' + - 'foo:ba:r\n' + - '------^'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but ":" found.\n' + 'foo:ba:r\n' + '------^' + ); }); it('should throw an error for range queries missing a value', () => { expect(() => { fromKueryExpression('foo > '); - }).toThrow('Expected literal, whitespace but end of input found.\n' + - 'foo > \n' + - '------^'); + }).toThrow('Expected literal, whitespace but end of input found.\n' + 'foo > \n' + '------^'); }); it('should throw an error for range queries missing a field', () => { expect(() => { fromKueryExpression('< 1000'); - }).toThrow('Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' + - '< 1000\n' + - '^'); + }).toThrow( + 'Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' + + '< 1000\n' + + '^' + ); }); - }); diff --git a/packages/kbn-es-query/src/kuery/errors/index.js b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts similarity index 55% rename from packages/kbn-es-query/src/kuery/errors/index.js rename to src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts index 82e1aee7b775a..7c90119fcc1bc 100644 --- a/packages/kbn-es-query/src/kuery/errors/index.js +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts @@ -20,35 +20,46 @@ import { repeat } from 'lodash'; import { i18n } from '@kbn/i18n'; -const endOfInputText = i18n.translate('kbnESQuery.kql.errors.endOfInputText', { +const endOfInputText = i18n.translate('data.common.esQuery.kql.errors.endOfInputText', { defaultMessage: 'end of input', }); -export class KQLSyntaxError extends Error { +const grammarRuleTranslations: Record = { + fieldName: i18n.translate('data.common.esQuery.kql.errors.fieldNameText', { + defaultMessage: 'field name', + }), + value: i18n.translate('data.common.esQuery.kql.errors.valueText', { + defaultMessage: 'value', + }), + literal: i18n.translate('data.common.esQuery.kql.errors.literalText', { + defaultMessage: 'literal', + }), + whitespace: i18n.translate('data.common.esQuery.kql.errors.whitespaceText', { + defaultMessage: 'whitespace', + }), +}; + +interface KQLSyntaxErrorData extends Error { + found: string; + expected: KQLSyntaxErrorExpected[]; + location: any; +} - constructor(error, expression) { - const grammarRuleTranslations = { - fieldName: i18n.translate('kbnESQuery.kql.errors.fieldNameText', { - defaultMessage: 'field name', - }), - value: i18n.translate('kbnESQuery.kql.errors.valueText', { - defaultMessage: 'value', - }), - literal: i18n.translate('kbnESQuery.kql.errors.literalText', { - defaultMessage: 'literal', - }), - whitespace: i18n.translate('kbnESQuery.kql.errors.whitespaceText', { - defaultMessage: 'whitespace', - }), - }; +interface KQLSyntaxErrorExpected { + description: string; +} + +export class KQLSyntaxError extends Error { + shortMessage: string; - const translatedExpectations = error.expected.map((expected) => { + constructor(error: KQLSyntaxErrorData, expression: any) { + const translatedExpectations = error.expected.map(expected => { return grammarRuleTranslations[expected.description] || expected.description; }); const translatedExpectationText = translatedExpectations.join(', '); - const message = i18n.translate('kbnESQuery.kql.errors.syntaxError', { + const message = i18n.translate('data.common.esQuery.kql.errors.syntaxError', { defaultMessage: 'Expected {expectedList} but {foundInput} found.', values: { expectedList: translatedExpectationText, @@ -56,11 +67,9 @@ export class KQLSyntaxError extends Error { }, }); - const fullMessage = [ - message, - expression, - repeat('-', error.location.start.offset) + '^', - ].join('\n'); + const fullMessage = [message, expression, repeat('-', error.location.start.offset) + '^'].join( + '\n' + ); super(fullMessage); this.name = 'KQLSyntaxError'; diff --git a/packages/kbn-es-query/src/kuery/node_types/function.js b/src/plugins/data/common/es_query/kuery/node_types/function.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/function.js rename to src/plugins/data/common/es_query/kuery/node_types/function.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.test.ts b/src/plugins/data/common/es_query/kuery/node_types/function.test.ts new file mode 100644 index 0000000000000..ca9798eb6e74f --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/function.test.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { fields } from '../../../index_patterns/mocks'; + +import { nodeTypes } from './index'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import { buildNode, buildNodeWithArgumentNodes, toElasticsearchQuery } from './function'; +// @ts-ignore +import { toElasticsearchQuery as isFunctionToElasticsearchQuery } from '../functions/is'; + +describe('kuery node types', () => { + describe('function', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNode', () => { + test('should return a node representing the given kuery function', () => { + const result = buildNode('is', 'extension', 'jpg'); + + expect(result).toHaveProperty('type', 'function'); + expect(result).toHaveProperty('function', 'is'); + expect(result).toHaveProperty('arguments'); + }); + }); + + describe('buildNodeWithArgumentNodes', () => { + test('should return a function node with the given argument list untouched', () => { + const fieldNameLiteral = nodeTypes.literal.buildNode('extension'); + const valueLiteral = nodeTypes.literal.buildNode('jpg'); + const argumentNodes = [fieldNameLiteral, valueLiteral]; + const result = buildNodeWithArgumentNodes('is', argumentNodes); + + expect(result).toHaveProperty('type', 'function'); + expect(result).toHaveProperty('function', 'is'); + expect(result).toHaveProperty('arguments'); + expect(result.arguments).toBe(argumentNodes); + expect(result.arguments).toEqual(argumentNodes); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return the given function type's ES query representation", () => { + const node = buildNode('is', 'extension', 'jpg'); + const expected = isFunctionToElasticsearchQuery(node, indexPattern); + const result = toElasticsearchQuery(node, indexPattern); + + expect(expected).toEqual(result); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/node_types/index.d.ts b/src/plugins/data/common/es_query/kuery/node_types/index.d.ts similarity index 72% rename from packages/kbn-es-query/src/kuery/node_types/index.d.ts rename to src/plugins/data/common/es_query/kuery/node_types/index.d.ts index daf8032f9fe0e..720d64e11a0f8 100644 --- a/packages/kbn-es-query/src/kuery/node_types/index.d.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/index.d.ts @@ -21,7 +21,8 @@ * WARNING: these typings are incomplete */ -import { JsonObject, JsonValue } from '..'; +import { IIndexPattern } from '../../../index_patterns'; +import { KueryNode, JsonValue } from '..'; type FunctionName = | 'is' @@ -34,6 +35,17 @@ type FunctionName = | 'geoPolygon' | 'nested'; +interface FunctionType { + buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + toElasticsearchQuery: ( + node: any, + indexPattern?: IIndexPattern, + config?: Record, + context?: Record + ) => JsonValue; +} + interface FunctionTypeBuildNode { type: 'function'; function: FunctionName; @@ -41,32 +53,40 @@ interface FunctionTypeBuildNode { arguments: any[]; } -interface FunctionType { - buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; - buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; - toElasticsearchQuery: (node: any, indexPattern: any, config: JsonObject) => JsonValue; -} - interface LiteralType { - buildNode: ( - value: null | boolean | number | string - ) => { type: 'literal'; value: null | boolean | number | string }; + buildNode: (value: null | boolean | number | string) => LiteralTypeBuildNode; toElasticsearchQuery: (node: any) => null | boolean | number | string; } +interface LiteralTypeBuildNode { + type: 'literal'; + value: null | boolean | number | string; +} + interface NamedArgType { - buildNode: (name: string, value: any) => { type: 'namedArg'; name: string; value: any }; + buildNode: (name: string, value: any) => NamedArgTypeBuildNode; toElasticsearchQuery: (node: any) => string; } +interface NamedArgTypeBuildNode { + type: 'namedArg'; + name: string; + value: any; +} + interface WildcardType { - buildNode: (value: string) => { type: 'wildcard'; value: string }; + buildNode: (value: string) => WildcardTypeBuildNode; test: (node: any, string: string) => boolean; toElasticsearchQuery: (node: any) => string; toQueryStringQuery: (node: any) => string; hasLeadingWildcard: (node: any) => boolean; } +interface WildcardTypeBuildNode { + type: 'wildcard'; + value: string; +} + interface NodeTypes { function: FunctionType; literal: LiteralType; diff --git a/packages/kbn-es-query/src/kuery/node_types/index.js b/src/plugins/data/common/es_query/kuery/node_types/index.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/index.js rename to src/plugins/data/common/es_query/kuery/node_types/index.js diff --git a/packages/kbn-es-query/src/kuery/node_types/literal.js b/src/plugins/data/common/es_query/kuery/node_types/literal.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/literal.js rename to src/plugins/data/common/es_query/kuery/node_types/literal.js diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js b/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js rename to src/plugins/data/common/es_query/kuery/node_types/literal.test.ts index 25fe2bcc45a45..60fe2d6d1013c 100644 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js +++ b/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts @@ -17,34 +17,27 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as literal from '../literal'; +// @ts-ignore +import { buildNode, toElasticsearchQuery } from './literal'; -describe('kuery node types', function () { +describe('kuery node types', () => { + describe('literal', () => { + describe('buildNode', () => { + test('should return a node representing the given value', () => { + const result = buildNode('foo'); - describe('literal', function () { - - describe('buildNode', function () { - - it('should return a node representing the given value', function () { - const result = literal.buildNode('foo'); - expect(result).to.have.property('type', 'literal'); - expect(result).to.have.property('value', 'foo'); + expect(result).toHaveProperty('type', 'literal'); + expect(result).toHaveProperty('value', 'foo'); }); - }); - describe('toElasticsearchQuery', function () { + describe('toElasticsearchQuery', () => { + test('should return the literal value represented by the given node', () => { + const node = buildNode('foo'); + const result = toElasticsearchQuery(node); - it('should return the literal value represented by the given node', function () { - const node = literal.buildNode('foo'); - const result = literal.toElasticsearchQuery(node); - expect(result).to.be('foo'); + expect(result).toBe('foo'); }); - }); - - }); - }); diff --git a/packages/kbn-es-query/src/kuery/node_types/named_arg.js b/src/plugins/data/common/es_query/kuery/node_types/named_arg.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/named_arg.js rename to src/plugins/data/common/es_query/kuery/node_types/named_arg.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts b/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts new file mode 100644 index 0000000000000..36c40d28e55c2 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { nodeTypes } from './index'; + +// @ts-ignore +import { buildNode, toElasticsearchQuery } from './named_arg'; + +describe('kuery node types', () => { + describe('named arg', () => { + describe('buildNode', () => { + test('should return a node representing a named argument with the given value', () => { + const result = buildNode('fieldName', 'foo'); + expect(result).toHaveProperty('type', 'namedArg'); + expect(result).toHaveProperty('name', 'fieldName'); + expect(result).toHaveProperty('value'); + + const literalValue = result.value; + expect(literalValue).toHaveProperty('type', 'literal'); + expect(literalValue).toHaveProperty('value', 'foo'); + }); + + test('should support literal nodes as values', () => { + const value = nodeTypes.literal.buildNode('foo'); + const result = buildNode('fieldName', value); + + expect(result.value).toBe(value); + expect(result.value).toEqual(value); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the argument value represented by the given node', () => { + const node = buildNode('fieldName', 'foo'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo'); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/node_types/wildcard.js b/src/plugins/data/common/es_query/kuery/node_types/wildcard.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/wildcard.js rename to src/plugins/data/common/es_query/kuery/node_types/wildcard.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts b/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts new file mode 100644 index 0000000000000..7e221d96b49e9 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts @@ -0,0 +1,110 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + buildNode, + wildcardSymbol, + hasLeadingWildcard, + toElasticsearchQuery, + test as testNode, + toQueryStringQuery, + // @ts-ignore +} from './wildcard'; + +describe('kuery node types', () => { + describe('wildcard', () => { + describe('buildNode', () => { + test('should accept a string argument representing a wildcard string', () => { + const wildcardValue = `foo${wildcardSymbol}bar`; + const result = buildNode(wildcardValue); + + expect(result).toHaveProperty('type', 'wildcard'); + expect(result).toHaveProperty('value', wildcardValue); + }); + + test('should accept and parse a wildcard string', () => { + const result = buildNode('foo*bar'); + + expect(result).toHaveProperty('type', 'wildcard'); + expect(result.value).toBe(`foo${wildcardSymbol}bar`); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the string representation of the wildcard literal', () => { + const node = buildNode('foo*bar'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo*bar'); + }); + }); + + describe('toQueryStringQuery', () => { + test('should return the string representation of the wildcard literal', () => { + const node = buildNode('foo*bar'); + const result = toQueryStringQuery(node); + + expect(result).toBe('foo*bar'); + }); + + test('should escape query_string query special characters other than wildcard', () => { + const node = buildNode('+foo*bar'); + const result = toQueryStringQuery(node); + + expect(result).toBe('\\+foo*bar'); + }); + }); + + describe('test', () => { + test('should return a boolean indicating whether the string matches the given wildcard node', () => { + const node = buildNode('foo*bar'); + + expect(testNode(node, 'foobar')).toBe(true); + expect(testNode(node, 'foobazbar')).toBe(true); + expect(testNode(node, 'foobar')).toBe(true); + expect(testNode(node, 'fooqux')).toBe(false); + expect(testNode(node, 'bazbar')).toBe(false); + }); + + test('should return a true even when the string has newlines or tabs', () => { + const node = buildNode('foo*bar'); + + expect(testNode(node, 'foo\nbar')).toBe(true); + expect(testNode(node, 'foo\tbar')).toBe(true); + }); + }); + + describe('hasLeadingWildcard', () => { + test('should determine whether a wildcard node contains a leading wildcard', () => { + const node = buildNode('foo*bar'); + expect(hasLeadingWildcard(node)).toBe(false); + + const leadingWildcardNode = buildNode('*foobar'); + expect(hasLeadingWildcard(leadingWildcardNode)).toBe(true); + }); + + // Lone wildcards become exists queries, so we aren't worried about their performance + test('should not consider a lone wildcard to be a leading wildcard', () => { + const leadingWildcardNode = buildNode('*'); + + expect(hasLeadingWildcard(leadingWildcardNode)).toBe(false); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/index.d.ts b/src/plugins/data/common/es_query/kuery/types.ts similarity index 73% rename from packages/kbn-es-query/src/kuery/index.d.ts rename to src/plugins/data/common/es_query/kuery/types.ts index b01a8914f68ef..86cb7e08a767c 100644 --- a/packages/kbn-es-query/src/kuery/index.d.ts +++ b/src/plugins/data/common/es_query/kuery/types.ts @@ -17,14 +17,29 @@ * under the License. */ -export * from './ast'; +import { NodeTypes } from './node_types'; + +export interface KueryNode { + type: keyof NodeTypes; + [key: string]: any; +} + +export type DslQuery = any; + +export interface KueryParseOptions { + helpers: { + [key: string]: any; + }; + startRule: string; + allowLeadingWildcards: boolean; + errorOnLuceneSyntax: boolean; +} + export { nodeTypes } from './node_types'; +export type JsonArray = JsonValue[]; export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; export interface JsonObject { [key: string]: JsonValue; } - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface JsonArray extends Array {} diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index 687870306c873..1c17e231cace8 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -17,10 +17,10 @@ * under the License. */ -import { FieldFormat } from '../field_format'; +import { FieldFormat, IFieldFormatType } from '../field_format'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; -export const createCustomFieldFormat = (convert: TextContextTypeConvert) => +export const createCustomFieldFormat = (convert: TextContextTypeConvert): IFieldFormatType => class CustomFieldFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.CUSTOM; diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 6b5f665c6e20e..dd445a33f21c5 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -73,7 +73,7 @@ export abstract class FieldFormat { */ public type: any = this.constructor; - private readonly _params: any; + protected readonly _params: any; protected getConfig: Function | undefined; constructor(_params: any = {}, getConfig?: Function) { diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 7c3e597ae12d2..a9d066f3cd49f 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -19,8 +19,8 @@ module.exports = { kuery: { - src: 'packages/kbn-es-query/src/kuery/ast/kuery.peg', - dest: 'packages/kbn-es-query/src/kuery/ast/kuery.js', + src: 'src/plugins/data/common/es_query/kuery/ast/kuery.peg', + dest: 'src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js', options: { allowedStartRules: ['start', 'Literal'] } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 52be4d4fba774..32fbe46ac560c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; // @ts-ignore @@ -16,13 +15,14 @@ import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; +import { usePlugins } from '../../../new-platform/plugin'; +import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { - AutocompleteSuggestion, AutocompleteProvider, + AutocompleteSuggestion, + esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; -import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; -import { usePlugins } from '../../../new-platform/plugin'; const Container = styled.div` margin-bottom: 10px; @@ -34,8 +34,8 @@ interface State { } function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } function getSuggestions( diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts index cee097d010212..a6f6d36ecfc81 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toElasticsearchQuery, fromKueryExpression } from '@kbn/es-query'; import { ESFilter } from '../../../../typings/elasticsearch'; import { UIFilters } from '../../../../typings/ui-filters'; import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; @@ -12,10 +11,13 @@ import { localUIFilters, localUIFilterNames } from '../../ui_filters/local_ui_filters/config'; -import { StaticIndexPattern } from '../../../../../../../../src/legacy/core_plugins/data/public'; +import { + esKuery, + IIndexPattern +} from '../../../../../../../../src/plugins/data/server'; export function getUiFiltersES( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFilters: UIFilters ) { const { kuery, environment, ...localFilterValues } = uiFilters; @@ -43,13 +45,13 @@ export function getUiFiltersES( } function getKueryUiFilterES( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, kuery?: string ) { if (!kuery || !indexPattern) { return; } - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern) as ESFilter; + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern) as ESFilter; } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts index 526728bd77cac..83c610800b89b 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { isEmpty } from 'lodash'; import { npStart } from 'ui/new_platform'; import { ElasticsearchAdapter } from './adapter_types'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { AutocompleteSuggestion, esKuery } from '../../../../../../../../src/plugins/data/public'; import { setup as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; const getAutocompleteProvider = (language: string) => @@ -20,7 +19,7 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { public isKueryValid(kuery: string): boolean { try { - fromKueryExpression(kuery); + esKuery.fromKueryExpression(kuery); } catch (err) { return false; } @@ -31,9 +30,9 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { if (!this.isKueryValid(kuery)) { return ''; } - const ast = fromKueryExpression(kuery); + const ast = esKuery.fromKueryExpression(kuery); const indexPattern = await this.getIndexPattern(); - return JSON.stringify(toElasticsearchQuery(ast, indexPattern)); + return JSON.stringify(esKuery.toElasticsearchQuery(ast, indexPattern)); } public async getSuggestions( kuery: string, diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx index 82e50c702997f..56458e5de273f 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx @@ -9,12 +9,9 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { connect } from 'react-redux'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { IDataPluginServices, Query } from 'src/plugins/data/public'; import { IndexPatternSavedObject, IndexPatternProvider } from '../types'; import { QueryBarInput, IndexPattern } from '../../../../../../src/legacy/core_plugins/data/public'; import { openSourceModal } from '../services/source_modal'; - import { GraphState, datasourceSelector, @@ -23,6 +20,7 @@ import { } from '../state_management'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { IDataPluginServices, Query, esKuery } from '../../../../../../src/plugins/data/public'; export interface OuterSearchBarProps { isLoading: boolean; @@ -44,7 +42,10 @@ export interface SearchBarProps extends OuterSearchBarProps { function queryToString(query: Query, indexPattern: IndexPattern) { if (query.language === 'kuery' && typeof query.query === 'string') { - const dsl = toElasticsearchQuery(fromKueryExpression(query.query as string), indexPattern); + const dsl = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query as string), + indexPattern + ); // JSON representation of query will be handled by existing logic. // TODO clean this up and handle it in the data fetch layer once // it moved to typescript. diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx index 1353b065bc444..a851f8380b915 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; @@ -12,6 +11,7 @@ import { StaticIndexPattern } from 'ui/index_patterns'; import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../autocomplete_field'; import { isDisplayable } from '../../utils/is_displayable'; +import { esKuery } from '../../../../../../../src/plugins/data/public'; interface Props { derivedIndexPattern: StaticIndexPattern; @@ -21,7 +21,7 @@ interface Props { function validateQuery(query: string) { try { - fromKueryExpression(query); + esKuery.fromKueryExpression(query); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts index 069f631b9c026..f17f7be4defe9 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts @@ -5,10 +5,8 @@ */ import { createSelector } from 'reselect'; - -import { fromKueryExpression } from '@kbn/es-query'; - import { LogFilterState } from './reducer'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; export const selectLogFilterQuery = (state: LogFilterState) => state.filterQuery ? state.filterQuery.query : null; @@ -23,7 +21,7 @@ export const selectIsLogFilterQueryDraftValid = createSelector( filterQueryDraft => { if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { try { - fromKueryExpression(filterQueryDraft.expression); + esKuery.fromKueryExpression(filterQueryDraft.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts index 7d518b5e20f2d..0acce82950f77 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts @@ -6,8 +6,7 @@ import { createSelector } from 'reselect'; -import { fromKueryExpression } from '@kbn/es-query'; - +import { esKuery } from '../../../../../../../../src/plugins/data/public'; import { WaffleFilterState } from './reducer'; export const selectWaffleFilterQuery = (state: WaffleFilterState) => @@ -23,7 +22,7 @@ export const selectIsWaffleFilterQueryDraftValid = createSelector( filterQueryDraft => { if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { try { - fromKueryExpression(filterQueryDraft.expression); + esKuery.fromKueryExpression(filterQueryDraft.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/utils/kuery.ts b/x-pack/legacy/plugins/infra/public/utils/kuery.ts index 4a767f2777512..2e793d53b4622 100644 --- a/x-pack/legacy/plugins/infra/public/utils/kuery.ts +++ b/x-pack/legacy/plugins/infra/public/utils/kuery.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { StaticIndexPattern } from 'ui/index_patterns'; +import { esKuery } from '../../../../../../src/plugins/data/public'; export const convertKueryToElasticSearchQuery = ( kueryExpression: string, @@ -13,7 +13,9 @@ export const convertKueryToElasticSearchQuery = ( ) => { try { return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) : ''; } catch (err) { return ''; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js index 8a82194470ace..7b0e42283d5f5 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js @@ -5,11 +5,11 @@ */ import { flatten, mapValues, uniq } from 'lodash'; -import { fromKueryExpression } from '@kbn/es-query'; import { getSuggestionsProvider as field } from './field'; import { getSuggestionsProvider as value } from './value'; import { getSuggestionsProvider as operator } from './operator'; import { getSuggestionsProvider as conjunction } from './conjunction'; +import { esKuery } from '../../../../../../src/plugins/data/public'; const cursorSymbol = '@kuery-cursor@'; @@ -27,7 +27,7 @@ export const kueryProvider = ({ config, indexPatterns, boolFilter }) => { let cursorNode; try { - cursorNode = fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); + cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); } catch (e) { cursorNode = {}; } diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js index 16e4a563c33ae..18b3382175fdd 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { npStart } from 'ui/new_platform'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; const getAutocompleteProvider = language => npStart.plugins.data.autocomplete.getProvider(language); @@ -35,8 +35,8 @@ export async function getSuggestions( } function convertKueryToEsQuery(kuery, indexPattern) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } // Recommended by MDN for escaping user input to be treated as a literal string within a regular expression // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions @@ -53,7 +53,7 @@ export function escapeDoubleQuotes(string) { } export function getKqlQueryValues(inputValue, indexPattern) { - const ast = fromKueryExpression(inputValue); + const ast = esKuery.fromKueryExpression(inputValue); const isAndOperator = (ast.function === 'and'); const query = convertKueryToEsQuery(inputValue, indexPattern); const filteredFields = []; diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts index b6819e54575d6..d82079dd05d31 100644 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery, JsonObject } from '@kbn/es-query'; import { isEmpty, isString, flow } from 'lodash/fp'; import { Query, esFilters, esQuery, + esKuery, IIndexPattern, } from '../../../../../../../src/plugins/data/public'; @@ -21,7 +21,9 @@ export const convertKueryToElasticSearchQuery = ( ) => { try { return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) : ''; } catch (err) { return ''; @@ -31,10 +33,10 @@ export const convertKueryToElasticSearchQuery = ( export const convertKueryToDslFilter = ( kueryExpression: string, indexPattern: IIndexPattern -): JsonObject => { +): esKuery.JsonObject => { try { return kueryExpression - ? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern) + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) : {}; } catch (err) { return {}; @@ -55,7 +57,7 @@ export const escapeQueryValue = (val: number | string = ''): string | number => export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { try { - fromKueryExpression(kqlFilterQuery.expression); + esKuery.fromKueryExpression(kqlFilterQuery.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx index 500557a2c2a96..58a9e57b32ce6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; -import { fromKueryExpression } from '@kbn/es-query'; import { isEmpty } from 'lodash/fp'; import React from 'react'; @@ -17,6 +16,7 @@ import { } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; import { ERROR_CODE } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; +import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; import * as CreateRuleI18n from '../../translations'; @@ -106,7 +106,7 @@ export const schema: FormSchema = { const { query } = value as FieldValueQueryBar; if (!isEmpty(query.query as string) && query.language === 'kuery') { try { - fromKueryExpression(query.query); + esKuery.fromKueryExpression(query.query); } catch (err) { return { code: 'ERR_FIELD_FORMAT', diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts index 16100089a9e56..b465392a50ae1 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts @@ -104,7 +104,7 @@ export function createSearchItems( const filters = fs.length ? fs : []; const esQueryConfigs = esQuery.getEsQueryConfig(config); - combinedQuery = esQuery.buildEsQuery(indexPattern || null, [query], filters, esQueryConfigs); + combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); } return { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx index f529c9cd9d53f..da392660eb70e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx @@ -9,14 +9,17 @@ import { uniqueId, startsWith } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { AutocompleteProviderRegister, AutocompleteSuggestion } from 'src/plugins/data/public'; -import { StaticIndexPattern } from 'src/legacy/core_plugins/data/public/index_patterns/index_patterns'; import { Typeahead } from './typeahead'; import { getIndexPattern } from '../../../lib/adapters/index_pattern'; import { UptimeSettingsContext } from '../../../contexts'; import { useUrlParams } from '../../../hooks'; import { toStaticIndexPattern } from '../../../lib/helper'; +import { + AutocompleteProviderRegister, + AutocompleteSuggestion, + esKuery, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; @@ -27,15 +30,15 @@ interface State { isLoadingIndexPattern: boolean; } -function convertKueryToEsQuery(kuery: string, indexPattern: unknown) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } function getSuggestions( query: string, selectionStart: number, - apmIndexPattern: StaticIndexPattern, + apmIndexPattern: IIndexPattern, autocomplete: Pick ) { const autocompleteProvider = autocomplete.getProvider('kuery'); diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index ded16c3f8eb2f..09d40d32b696c 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -6,10 +6,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import React, { Fragment, useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { AutocompleteProviderRegister } from 'src/plugins/data/public'; import { getOverviewPageBreadcrumbs } from '../breadcrumbs'; import { EmptyState, @@ -26,6 +24,7 @@ import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; import { getIndexPattern } from '../lib/adapters/index_pattern'; import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper'; +import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public'; interface OverviewPageProps { basePath: string; @@ -109,8 +108,8 @@ export const OverviewPage = ({ if (indexPattern) { const staticIndexPattern = toStaticIndexPattern(indexPattern); const combinedFilterString = combineFiltersAndUserSearch(filterQueryString, kueryString); - const ast = fromKueryExpression(combinedFilterString); - const elasticsearchQuery = toElasticsearchQuery(ast, staticIndexPattern); + const ast = esKuery.fromKueryExpression(combinedFilterString); + const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, staticIndexPattern); filters = JSON.stringify(elasticsearchQuery); } } diff --git a/x-pack/package.json b/x-pack/package.json index eccc5918e6d50..d97fd38676bde 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -185,7 +185,6 @@ "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/elastic-idx": "1.0.0", - "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/ui-framework": "1.0.0", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f5fc453557122..217b20797492a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2527,12 +2527,12 @@ "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", "kbnDocViews.table.toggleFieldDetails": "フィールド詳細を切り替える", - "kbnESQuery.kql.errors.endOfInputText": "インプットの終わり", - "kbnESQuery.kql.errors.fieldNameText": "フィールド名", - "kbnESQuery.kql.errors.literalText": "文字通り", - "kbnESQuery.kql.errors.syntaxError": "{expectedList} が予測されましたが {foundInput} が検出されました。", - "kbnESQuery.kql.errors.valueText": "値", - "kbnESQuery.kql.errors.whitespaceText": "ホワイトスペース", + "data.common.esQuery.kql.errors.endOfInputText": "インプットの終わり", + "data.common.esQuery.kql.errors.fieldNameText": "フィールド名", + "data.common.esQuery.kql.errors.literalText": "文字通り", + "data.common.esQuery.kql.errors.syntaxError": "{expectedList} が予測されましたが {foundInput} が検出されました。", + "data.common.esQuery.kql.errors.valueText": "値", + "data.common.esQuery.kql.errors.whitespaceText": "ホワイトスペース", "kbnVislibVisTypes.area.areaDescription": "折れ線グラフの下の数量を強調します。", "kbnVislibVisTypes.area.areaTitle": "エリア", "kbnVislibVisTypes.area.groupTitle": "系列を分割", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 288fc92be3cbd..6a2ba20af7714 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2528,12 +2528,12 @@ "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", "kbnDocViews.table.toggleFieldDetails": "切换字段详细信息", - "kbnESQuery.kql.errors.endOfInputText": "输入结束", - "kbnESQuery.kql.errors.fieldNameText": "字段名称", - "kbnESQuery.kql.errors.literalText": "文本", - "kbnESQuery.kql.errors.syntaxError": "应为 {expectedList},但却找到了 {foundInput}。", - "kbnESQuery.kql.errors.valueText": "值", - "kbnESQuery.kql.errors.whitespaceText": "空白", + "data.common.esQuery.kql.errors.endOfInputText": "输入结束", + "data.common.esQuery.kql.errors.fieldNameText": "字段名称", + "data.common.esQuery.kql.errors.literalText": "文本", + "data.common.esQuery.kql.errors.syntaxError": "应为 {expectedList},但却找到了 {foundInput}。", + "data.common.esQuery.kql.errors.valueText": "值", + "data.common.esQuery.kql.errors.whitespaceText": "空白", "kbnVislibVisTypes.area.areaDescription": "突出折线图下方的数量", "kbnVislibVisTypes.area.areaTitle": "面积图", "kbnVislibVisTypes.area.groupTitle": "拆分序列", From a234e8b836007754240b4e215b865df1ad0e5fee Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 26 Nov 2019 12:36:35 -0800 Subject: [PATCH 098/128] [DOCS] Fixes broken links (#51634) --- docs/api/role-management/put.asciidoc | 4 ++- docs/developer/security/rbac.asciidoc | 9 ++++++- .../tutorial-full-experience.asciidoc | 2 +- .../tutorial-sample-data.asciidoc | 4 +-- docs/management/managing-indices.asciidoc | 2 +- docs/setup/install.asciidoc | 4 +-- .../security/authentication/index.asciidoc | 25 ++++++++++--------- docs/user/security/reporting.asciidoc | 8 +++--- docs/user/security/securing-kibana.asciidoc | 2 +- 9 files changed, 35 insertions(+), 25 deletions(-) diff --git a/docs/api/role-management/put.asciidoc b/docs/api/role-management/put.asciidoc index 67ec15892afe4..a00fedf7e7ac4 100644 --- a/docs/api/role-management/put.asciidoc +++ b/docs/api/role-management/put.asciidoc @@ -26,7 +26,9 @@ To use the create or update role API, you must have the `manage_security` cluste (Optional, object) In the `metadata` object, keys that begin with `_` are reserved for system usage. `elasticsearch`:: - (Optional, object) {es} cluster and index privileges. Valid keys include `cluster`, `indices`, and `run_as`. For more information, see {xpack-ref}/defining-roles.html[Defining Roles]. + (Optional, object) {es} cluster and index privileges. Valid keys include + `cluster`, `indices`, and `run_as`. For more information, see + {ref}/defining-roles.html[Defining roles]. `kibana`:: (list) Objects that specify the <> for the role: diff --git a/docs/developer/security/rbac.asciidoc b/docs/developer/security/rbac.asciidoc index b967dabf0684f..02b8233a9a3df 100644 --- a/docs/developer/security/rbac.asciidoc +++ b/docs/developer/security/rbac.asciidoc @@ -1,7 +1,14 @@ [[development-security-rbac]] === Role-based access control -Role-based access control (RBAC) in {kib} relies upon the {xpack-ref}/security-privileges.html#application-privileges[application privileges] that Elasticsearch exposes. This allows {kib} to define the privileges that {kib} wishes to grant to users, assign them to the relevant users using roles, and then authorize the user to perform a specific action. This is handled within a secured instance of the `SavedObjectsClient` and available transparently to consumers when using `request.getSavedObjectsClient()` or `savedObjects.getScopedSavedObjectsClient()`. +Role-based access control (RBAC) in {kib} relies upon the +{ref}/security-privileges.html#application-privileges[application privileges] +that Elasticsearch exposes. This allows {kib} to define the privileges that +{kib} wishes to grant to users, assign them to the relevant users using roles, +and then authorize the user to perform a specific action. This is handled within +a secured instance of the `SavedObjectsClient` and available transparently to +consumers when using `request.getSavedObjectsClient()` or +`savedObjects.getScopedSavedObjectsClient()`. [[development-rbac-privileges]] ==== {kib} Privileges diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc index eafbb7d8f7c91..a05205fceab4a 100644 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ b/docs/getting-started/tutorial-full-experience.asciidoc @@ -91,7 +91,7 @@ and whether it's _tokenized_, or broken up into separate words. NOTE: If security is enabled, you must have the `all` Kibana privilege to run this tutorial. You must also have the `create`, `manage` `read`, `write,` and `delete` -index privileges. See {xpack-ref}/security-privileges.html[Security Privileges] +index privileges. See {ref}/security-privileges.html[Security privileges] for more information. In Kibana *Dev Tools > Console*, set up a mapping for the Shakespeare data set: diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc index 24cc176d5daf9..f41c648a3d492 100644 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ b/docs/getting-started/tutorial-sample-data.asciidoc @@ -12,8 +12,8 @@ with Kibana sample data and learn to: NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. See {xpack-ref}/security-privileges.html[Security Privileges] -for more information. +on the `kibana_sample_data_*` indices. See +{ref}/security-privileges.html[Security privileges] for more information. [float] diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc index 4a736e3ddab59..4c7f6c2aee6e6 100644 --- a/docs/management/managing-indices.asciidoc +++ b/docs/management/managing-indices.asciidoc @@ -22,7 +22,7 @@ If security is enabled, you must have the `monitor` cluster privilege and the `view_index_metadata` and `manage` index privileges to view the data. For index templates, you must have the `manage_index_templates` cluster privilege. -See {xpack-ref}/security-privileges.html[Security Privileges] for more +See {ref}/security-privileges.html[Security privileges] for more information. Before using this feature, you should be familiar with index management diff --git a/docs/setup/install.asciidoc b/docs/setup/install.asciidoc index b0893a6e78945..286fed34f64c5 100644 --- a/docs/setup/install.asciidoc +++ b/docs/setup/install.asciidoc @@ -54,8 +54,8 @@ Formulae are available from the Elastic Homebrew tap for installing {kib} on mac <> IMPORTANT: If your Elasticsearch installation is protected by -{xpack-ref}/elasticsearch-security.html[{security}] see -{kibana-ref}/using-kibana-with-security.html[Configuring Security in Kibana] for +{ref}/elasticsearch-security.html[{security}] see +{kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana] for additional setup instructions. include::install/targz.asciidoc[] diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index e6b70fa059fc2..32f341a9c1b7c 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -14,16 +14,17 @@ - <> [[basic-authentication]] -==== Basic Authentication +==== Basic authentication Basic authentication requires a username and password to successfully log in to {kib}. It is enabled by default and based on the Native security realm provided by {es}. The basic authentication provider uses a Kibana provided login form, and supports authentication using the `Authorization` request header's `Basic` scheme. The session cookies that are issued by the basic authentication provider are stateless. Therefore, logging out of Kibana when using the basic authentication provider clears the session cookies from the browser but does not invalidate the session cookie for reuse. -For more information about basic authentication and built-in users, see {xpack-ref}/setting-up-authentication.html[Setting Up User Authentication]. +For more information about basic authentication and built-in users, see +{ref}/setting-up-authentication.html[User authentication]. [[token-authentication]] -==== Token Authentication +==== Token authentication Token authentication allows users to login using the same Kibana provided login form as basic authentication. The token authentication provider is built on {es}'s token APIs. The bearer tokens returned by {es}'s {ref}/security-api-get-token.html[get token API] can be used directly with Kibana using the `Authorization` request header with the `Bearer` scheme. @@ -46,7 +47,7 @@ xpack.security.authc.providers: [token, basic] -------------------------------------------------------------------------------- [[pki-authentication]] -==== Public Key Infrastructure (PKI) Authentication +==== Public key infrastructure (PKI) authentication [IMPORTANT] ============================================================================ @@ -76,9 +77,9 @@ xpack.security.authc.providers: [pki, basic] Note that with `server.ssl.clientAuthentication` set to `required`, users are asked to provide a valid client certificate, even if they want to authenticate with username and password. Depending on the security policies, it may or may not be desired. If not, `server.ssl.clientAuthentication` can be set to `optional`. In this case, {kib} still requests a client certificate, but the client won't be required to present one. The `optional` client authentication mode might also be needed in other cases, for example, when PKI authentication is used in conjunction with Reporting. [[saml]] -==== SAML Single Sign-On +==== SAML single sign-on -SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {xpack-ref}/saml-guide.html[Configuring SAML Single-Sign-On on the Elastic Stack]. +SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {ref}/saml-guide.html[Configuring SAML single sign-on on the Elastic Stack]. Set the configuration values in `kibana.yml` as follows: @@ -106,7 +107,7 @@ server.xsrf.whitelist: [/api/security/saml/callback] Users will be able to log in to {kib} via SAML Single Sign-On by navigating directly to the {kib} URL. Users who aren't authenticated are redirected to the Identity Provider for login. Most Identity Providers maintain a long-lived session—users who logged in to a different application using the same Identity Provider in the same browser are automatically authenticated. An exception is if {es} or the Identity Provider is configured to force user to re-authenticate. This login scenario is called _Service Provider initiated login_. [float] -===== SAML and Basic Authentication +===== SAML and basic authentication SAML support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both SAML and Basic authentication for the same {kib} instance: @@ -135,7 +136,7 @@ xpack.security.authc.saml.maxRedirectURLSize: 1kb -------------------------------------------------------------------------------- [[oidc]] -==== OpenID Connect Single Sign-On +==== OpenID Connect single sign-on Similar to SAML, authentication with OpenID Connect allows users to log in to {kib} using an OpenID Connect Provider such as Google, or Okta. OpenID Connect should also be configured in {es}. For more details, see {ref}/oidc-guide.html[Configuring single sign-on to the {stack} using OpenID Connect]. @@ -166,7 +167,7 @@ server.xsrf.whitelist: [/api/security/v1/oidc] -------------------------------------------------------------------------------- [float] -===== OpenID Connect and Basic Authentication +===== OpenID Connect and basic authentication Similar to SAML, OpenID Connect support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both OpenID Connect and Basic authentication for the same {kib} instance: @@ -179,12 +180,12 @@ xpack.security.authc.providers: [oidc, basic] Users will be able to access the login page and use Basic authentication by navigating to the `/login` URL. [float] -==== Single Sign-On provider details +==== Single sign-on provider details The following sections apply both to <> and <> [float] -===== Access and Refresh Tokens +===== Access and refresh tokens Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider @@ -202,7 +203,7 @@ If {kib} can't redirect the user to the external authentication provider (for ex indicates that both access and refresh tokens are expired. Reloading the current {kib} page fixes the error. [float] -===== Local and Global Logout +===== Local and global logout During logout, both the {kib} session cookie and access/refresh token pair are invalidated. Even if the cookie has been leaked, it can't be re-used after logout. This is known as "local" logout. diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index fb40dc17c0abd..aaba60ca4b3ca 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -8,7 +8,7 @@ user actions in {kib}. To use {reporting} with {security} enabled, you need to <>. If you are automatically generating reports with -{xpack-ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} +{ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} to trust the {kib} server's certificate. For more information, see <>. @@ -35,7 +35,7 @@ POST /_security/user/reporter * If you are using an LDAP or Active Directory realm, you can either assign roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in -{xpack-ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. +{ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the `kibana_user` and `reporting_user` roles: + @@ -55,7 +55,7 @@ In a production environment, you should restrict access to the {reporting} endpoints to authorized users. This requires that you: . Enable {security} on your {es} cluster. For more information, -see {xpack-ref}/security-getting-started.html[Getting Started with Security]. +see {ref}/security-getting-started.html[Getting Started with Security]. . Configure an SSL certificate for Kibana. For more information, see <>. . Configure {watcher} to trust the Kibana server's certificate by adding it to @@ -83,4 +83,4 @@ includes a watch that submits requests as the built-in `elastic` user: <>. For more information about configuring watches, see -{xpack-ref}/how-watcher-works.html[How Watcher Works]. +{ref}/how-watcher-works.html[How Watcher works]. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 2fbc6ba4f1ee6..60f5473f43b9d 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -121,7 +121,7 @@ TIP: You can define as many different roles for your {kib} users as you need. For example, create roles that have `read` and `view_index_metadata` privileges on specific index patterns. For more information, see -{xpack-ref}/authorization.html[Configuring Role-based Access Control]. +{ref}/authorization.html[User authorization]. -- From 9d8c93158c2532e636f49c9cb13b92aedefc90d3 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 26 Nov 2019 16:19:13 -0700 Subject: [PATCH 099/128] [SIEM][Detection Engine] Adds signal to ECS event.kind and fixes status in signals (#51772) ## Summary * Adds signal to the ECS event.kind when it copies over a signal * Creates a `original_event` if needed within signal so additional look ups don't have to happen * Fixes a bug with `signal.status` where it was not plumbed correctly * Adds extra unit tests around the filter * Adds missing unit tests around utils I didn't add before * Fixes a typing issue with output of a signal Example signal output: Original event turns into this: ```ts "event" : { "dataset" : "socket", "kind" : "signal", "action" : "existing_socket", "id" : "ffec6797-b92f-4436-bb40-69bac2c21874", "module" : "system" }, ``` Signal amplification turns into this where it contains original_event looks like this: ```ts "signal" : { "parent" : { "id" : "xNRlqW4BHe9nqdOi2358", "type" : "event", "index" : "auditbeat", "depth" : 1 }, "original_time" : "2019-11-26T20:27:11.169Z", "status" : "open", "rule" : { "id" : "643fbd2f-a3c9-449e-ba95-e3d89000a72a", "rule_id" : "rule-1", "false_positives" : [ ], "max_signals" : 100, "risk_score" : 1, "description" : "Detecting root and admin users", "from" : "now-6m", "immutable" : false, "interval" : "5m", "language" : "kuery", "name" : "Detect Root/Admin Users", "query" : "user.name: root or user.name: admin", "references" : [ "http://www.example.com", "https://ww.example.com" ], "severity" : "high", "tags" : [ ], "type" : "query", "to" : "now", "enabled" : true, "created_by" : "elastic_some_user", "updated_by" : "elastic_some_user" }, "original_event" : { "dataset" : "socket", "kind" : "state", "action" : "existing_socket", "id" : "ffec6797-b92f-4436-bb40-69bac2c21874", "module" : "system" } } ``` ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../alerts/__mocks__/es_results.ts | 57 +- .../alerts/get_filter.test.ts | 98 ++++ .../lib/detection_engine/alerts/types.ts | 4 - .../lib/detection_engine/alerts/utils.test.ts | 536 +++++++++++++++++- .../lib/detection_engine/alerts/utils.ts | 26 +- .../lib/detection_engine/signals_mapping.json | 72 +++ .../legacy/plugins/siem/server/lib/types.ts | 7 +- 7 files changed, 750 insertions(+), 50 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 079d3658461fa..8080bd5ddd913 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -4,10 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse, RuleTypeParams } from '../types'; +import { + SignalSourceHit, + SignalSearchResponse, + RuleTypeParams, + OutputRuleAlertRest, +} from '../types'; export const sampleRuleAlertParams = ( - maxSignals: number | undefined, + maxSignals?: number | undefined, riskScore?: number | undefined ): RuleTypeParams => ({ ruleId: 'rule-1', @@ -32,7 +37,7 @@ export const sampleRuleAlertParams = ( meta: undefined, }); -export const sampleDocNoSortId = (someUuid: string): SignalSourceHit => ({ +export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -44,7 +49,7 @@ export const sampleDocNoSortId = (someUuid: string): SignalSourceHit => ({ }, }); -export const sampleDocNoSortIdNoVersion = (someUuid: string): SignalSourceHit => ({ +export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -55,7 +60,7 @@ export const sampleDocNoSortIdNoVersion = (someUuid: string): SignalSourceHit => }, }); -export const sampleDocWithSortId = (someUuid: string): SignalSourceHit => ({ +export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -138,7 +143,9 @@ export const sampleBulkCreateDuplicateResult = { ], }; -export const sampleDocSearchResultsNoSortId = (someUuid: string): SignalSearchResponse => ({ +export const sampleDocSearchResultsNoSortId = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -159,7 +166,7 @@ export const sampleDocSearchResultsNoSortId = (someUuid: string): SignalSearchRe }); export const sampleDocSearchResultsNoSortIdNoVersion = ( - someUuid: string + someUuid: string = sampleIdGuid ): SignalSearchResponse => ({ took: 10, timed_out: false, @@ -180,7 +187,9 @@ export const sampleDocSearchResultsNoSortIdNoVersion = ( }, }); -export const sampleDocSearchResultsNoSortIdNoHits = (someUuid: string): SignalSearchResponse => ({ +export const sampleDocSearchResultsNoSortIdNoHits = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -222,7 +231,9 @@ export const repeatedSearchResultsWithSortId = ( }, }); -export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearchResponse => ({ +export const sampleDocSearchResultsWithSortId = ( + someUuid: string = sampleIdGuid +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -243,3 +254,31 @@ export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearch }); export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; +export const sampleIdGuid = 'e1e08ddc-5e37-49ff-a258-5393aa44435a'; + +export const sampleRule = (): Partial => { + return { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts index 9f72da44e963b..c55c99fb291c4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts @@ -7,6 +7,7 @@ import { getQueryFilter, getFilter } from './get_filter'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { AlertServices } from '../../../../../alerting/server/types'; +import { PartialFilter } from './types'; describe('get_filter', () => { let savedObjectsClient = savedObjectsClientMock.create(); @@ -145,6 +146,103 @@ describe('get_filter', () => { }); }); + test('it should work with a simple filter as a kuery without meta information', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [ + { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a kuery without meta information with an exists', () => { + const query: PartialFilter = { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + } as PartialFilter; + + const exists: PartialFilter = { + exists: { + field: 'host.hostname', + }, + } as PartialFilter; + + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [query, exists], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + { + exists: { + field: 'host.hostname', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + test('it should work with a simple filter that is disabled as a kuery', () => { const esQuery = getQueryFilter( 'host.name: windows', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 28431b8165266..462a9b7d65ee2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -65,10 +65,6 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & { updated_by: string | undefined | null; }; -export type OutputRuleES = OutputRuleAlertRest & { - status: 'open' | 'closed'; -}; - export type UpdateRuleAlertParamsRest = Partial & { id: string | undefined; rule_id: RuleAlertParams['ruleId'] | undefined; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index 19c8d5ccc87ca..fc50e54e06e4e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -14,6 +14,9 @@ import { singleBulkCreate, singleSearchAfter, searchAfterAndBulkCreate, + buildEventTypeSignal, + buildSignal, + buildRule, } from './utils'; import { sampleDocNoSortId, @@ -26,8 +29,12 @@ import { repeatedSearchResultsWithSortId, sampleBulkCreateDuplicateResult, sampleRuleGuid, + sampleRule, + sampleIdGuid, } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; +import { OutputRuleAlertRest } from './types'; +import { Signal } from '../../types'; const mockLogger: Logger = { log: jest.fn(), @@ -51,10 +58,9 @@ describe('utils', () => { }); describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { - const fakeUuid = uuid.v4(); - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const fakeSignalSourceHit = buildBulkBody({ - doc: sampleDocNoSortId(fakeUuid), + doc: sampleDocNoSortId(), ruleParams: sampleParams, id: sampleRuleGuid, name: 'rule-name', @@ -65,18 +71,225 @@ describe('utils', () => { }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; - if (fakeSignalSourceHit.signal.parent) { - delete fakeSignalSourceHit.signal.parent.id; - } expect(fakeSignalSourceHit).toEqual({ someKey: 'someValue', + event: { + kind: 'signal', + }, signal: { parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + kind: 'event', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'signal', + module: 'system', + }, + signal: { + original_event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with but no kind information', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'signal', + module: 'system', + }, + signal: { + original_event: { + action: 'socket_opened', + dataset: 'socket', + module: 'system', + }, + parent: { + id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', depth: 1, }, original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with with only kind information', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + kind: 'event', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + kind: 'signal', + }, + signal: { + original_event: { + kind: 'event', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', rule: { id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', @@ -96,7 +309,6 @@ describe('utils', () => { severity: 'high', tags: ['some fake tag'], type: 'query', - status: 'open', to: 'now', enabled: true, created_by: 'elastic', @@ -213,8 +425,7 @@ describe('utils', () => { }); }); test('create successful bulk create', async () => { - const fakeUuid = uuid.v4(); - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -226,7 +437,7 @@ describe('utils', () => { ], }); const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(fakeUuid), + someResult: sampleSearchResult(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -241,8 +452,7 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create successful bulk create with docs with no versioning', async () => { - const fakeUuid = uuid.v4(); - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -254,7 +464,7 @@ describe('utils', () => { ], }); const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(fakeUuid), + someResult: sampleSearchResult(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -269,7 +479,7 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create unsuccessful bulk create due to empty search results', async () => { - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); const successfulsingleBulkCreate = await singleBulkCreate({ @@ -288,12 +498,11 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create successful bulk create when bulk create has errors', async () => { - const fakeUuid = uuid.v4(); - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(fakeUuid), + someResult: sampleSearchResult(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -312,7 +521,7 @@ describe('utils', () => { describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ @@ -326,7 +535,7 @@ describe('utils', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); const searchAfterResult = await singleSearchAfter({ searchAfterSortId, @@ -339,7 +548,7 @@ describe('utils', () => { }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); @@ -356,7 +565,7 @@ describe('utils', () => { }); describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { - const sampleParams = sampleRuleAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(); const result = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults, ruleParams: sampleParams, @@ -446,8 +655,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { - const sampleParams = sampleRuleAlertParams(undefined); - const someUuid = uuid.v4(); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -458,7 +666,7 @@ describe('utils', () => { ], }); const result = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortId(someUuid), + someResult: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -475,8 +683,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleRuleAlertParams(undefined); - const someUuid = uuid.v4(); + const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValueOnce({ took: 100, errors: false, @@ -487,7 +694,7 @@ describe('utils', () => { ], }); const result = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortIdNoHits(someUuid), + someResult: sampleDocSearchResultsNoSortIdNoHits(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -504,7 +711,6 @@ describe('utils', () => { }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { const sampleParams = sampleRuleAlertParams(10); - const oneGuid = uuid.v4(); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -516,7 +722,7 @@ describe('utils', () => { }, ], }) - .mockReturnValueOnce(sampleDocSearchResultsNoSortId(oneGuid)); + .mockReturnValueOnce(sampleDocSearchResultsNoSortId()); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, @@ -596,4 +802,276 @@ describe('utils', () => { expect(result).toEqual(false); }); }); + + describe('buildEventTypeSignal', () => { + test('it returns the event appended of kind signal if it does not exist', () => { + const doc = sampleDocNoSortId(); + delete doc._source.event; + const eventType = buildEventTypeSignal(doc); + const expected: object = { kind: 'signal' }; + expect(eventType).toEqual(expected); + }); + + test('it returns the event appended of kind signal if it is an empty object', () => { + const doc = sampleDocNoSortId(); + doc._source.event = {}; + const eventType = buildEventTypeSignal(doc); + const expected: object = { kind: 'signal' }; + expect(eventType).toEqual(expected); + }); + + test('it returns the event with kind signal and other properties if they exist', () => { + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + }; + const eventType = buildEventTypeSignal(doc); + const expected: object = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + kind: 'signal', + }; + expect(eventType).toEqual(expected); + }); + }); + + describe('buildSignal', () => { + test('it builds a signal as expected without original_event if event does not exist', () => { + const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + delete doc._source.event; + const rule: Partial = sampleRule(); + const signal = buildSignal(doc, rule); + const expected: Signal = { + parent: { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }, + }; + expect(signal).toEqual(expected); + }); + + test('it builds a signal as expected with original_event if is present', () => { + const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + doc._source.event = { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }; + const rule: Partial = sampleRule(); + const signal = buildSignal(doc, rule); + const expected: Signal = { + parent: { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + original_event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + status: 'open', + rule: { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + }, + }; + expect(signal).toEqual(expected); + }); + }); + + describe('buildRule', () => { + test('it builds a rule as expected with filters present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = [ + { + query: 'host.name: Rebecca', + }, + { + query: 'host.name: Evan', + }, + { + query: 'host.name: Braden', + }, + ]; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: false, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: false, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + filters: [ + { + query: 'host.name: Rebecca', + }, + { + query: 'host.name: Evan', + }, + { + query: 'host.name: Braden', + }, + ], + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if enabled is null if is present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = undefined; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if filters is undefined if is present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = undefined; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag'], + to: 'now', + type: 'query', + updated_by: 'elastic', + }; + expect(rule).toEqual(expected); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index ba3f310c886ce..c3988b8fea458 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -14,7 +14,7 @@ import { SignalSearchResponse, BulkResponse, RuleTypeParams, - OutputRuleES, + OutputRuleAlertRest, } from './types'; import { buildEventsSearchQuery } from './build_events_query'; @@ -36,10 +36,9 @@ export const buildRule = ({ createdBy, updatedBy, interval, -}: BuildRuleParams): Partial => { - return pickBy((value: unknown) => value != null, { +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { id, - status: 'open', rule_id: ruleParams.ruleId, false_positives: ruleParams.falsePositives, saved_id: ruleParams.savedId, @@ -68,8 +67,8 @@ export const buildRule = ({ }); }; -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { - return { +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { + const signal: Signal = { parent: { id: doc._id, type: 'event', @@ -77,8 +76,13 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial): depth: 1, }, original_time: doc._source['@timestamp'], + status: 'open', rule, }; + if (doc._source.event != null) { + return { ...signal, original_event: doc._source.event }; + } + return signal; }; interface BuildBulkBodyParams { @@ -92,6 +96,14 @@ interface BuildBulkBodyParams { enabled: boolean; } +export const buildEventTypeSignal = (doc: SignalSourceHit): object => { + if (doc._source.event != null && doc._source.event instanceof Object) { + return { ...doc._source.event, kind: 'signal' }; + } else { + return { kind: 'signal' }; + } +}; + // format search_after result for signals index. export const buildBulkBody = ({ doc, @@ -113,9 +125,11 @@ export const buildBulkBody = ({ interval, }); const signal = buildSignal(doc, rule); + const event = buildEventTypeSignal(doc); const signalHit: SignalHit = { ...doc._source, '@timestamp': new Date().toISOString(), + event, signal, }; return signalHit; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json index a95c9625a0003..dfe3caed5b71a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json @@ -107,6 +107,78 @@ }, "original_time": { "type": "date" + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" } } }, diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 9c0059d0d109d..c53805dc95fe7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -23,7 +23,7 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { RuleAlertParamsRest } from './detection_engine/alerts/types'; +import { SearchTypes, OutputRuleAlertRest } from './detection_engine/alerts/types'; export * from './hosts'; @@ -66,7 +66,7 @@ export interface SiemContext { } export interface Signal { - rule: Partial; + rule: Partial; parent: { id: string; type: string; @@ -74,10 +74,13 @@ export interface Signal { depth: number; }; original_time: string; + original_event?: SearchTypes; + status: 'open' | 'closed'; } export interface SignalHit { '@timestamp': string; + event: object; signal: Partial; } From 0039e97747dffda32d3bad62986c72691cb24699 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 27 Nov 2019 01:55:48 +0200 Subject: [PATCH 100/128] [Telemetry] collector set to np (#51618) * first iteration * local collection ready * type check * fix collectorSet tests * unskip test * ordering * collectors as array in constructor * update README files * update README and canvas to check for optional dep * update README with more details * Add file path for README example * type UsageCollectionSetup * run type check after refactor --- .github/CODEOWNERS | 1 + src/core/CONVENTIONS.md | 37 ++++ src/legacy/core_plugins/kibana/index.js | 5 +- .../lib/csp_usage_collector/csp_collector.ts | 8 +- .../make_kql_usage_collector.js | 6 +- .../make_kql_usage_collector.test.js | 15 +- src/legacy/core_plugins/telemetry/README.md | 9 + src/legacy/core_plugins/telemetry/index.ts | 21 +- .../telemetry/server/collection_manager.ts | 5 +- .../telemetry/server/collectors/index.ts | 8 +- .../server/collectors/localization/index.ts | 2 +- .../telemetry_localization_collector.ts | 15 +- .../collectors/telemetry_plugin/index.ts | 2 +- .../telemetry_plugin_collector.ts | 16 +- .../server/collectors/ui_metric/index.ts | 2 +- .../telemetry_ui_metric_collector.ts | 8 +- .../server/collectors/usage/index.ts | 2 +- .../usage/telemetry_usage_collector.test.ts | 22 +- .../usage/telemetry_usage_collector.ts | 35 ++- .../core_plugins/telemetry/server/index.ts | 2 +- .../core_plugins/telemetry/server/plugin.ts | 20 +- .../__tests__/get_local_stats.js | 20 +- .../server/telemetry_collection/get_kibana.js | 11 +- .../telemetry_collection/get_local_stats.ts | 5 +- src/legacy/server/kbn_server.d.ts | 10 +- src/legacy/server/kbn_server.js | 2 - .../server/sample_data/usage/collector.ts | 25 +-- .../collectors/get_ops_stats_collector.js | 12 +- src/legacy/server/status/collectors/index.js | 2 +- src/legacy/server/status/index.js | 10 +- .../status/routes/api/register_stats.js | 17 +- src/legacy/server/usage/README.md | 92 -------- .../server/usage/classes/collector_set.js | 206 ----------------- src/plugins/usage_collection/README.md | 139 ++++++++++++ .../usage_collection/common/constants.ts} | 2 +- src/plugins/usage_collection/kibana.json | 7 + .../collector}/__tests__/collector_set.js | 38 ++-- .../server/collector}/collector.js | 7 +- .../server/collector/collector_set.ts | 209 ++++++++++++++++++ .../server/collector/index.ts} | 2 + .../server/collector}/usage_collector.js | 8 +- .../usage_collection/server/config.ts} | 17 +- .../usage_collection/server/index.ts} | 18 +- src/plugins/usage_collection/server/plugin.ts | 55 +++++ x-pack/legacy/plugins/apm/index.ts | 3 +- .../apm/server/lib/apm_telemetry/index.ts | 11 +- .../plugins/apm/server/routes/typings.ts | 8 +- .../canvas/__tests__/fixtures/kibana.js | 6 - x-pack/legacy/plugins/canvas/server/plugin.ts | 2 +- x-pack/legacy/plugins/canvas/server/shim.ts | 5 +- .../plugins/canvas/server/usage/collector.ts | 14 +- ....test.ts => cloud_usage_collector.test.ts} | 52 ++--- ..._collector.ts => cloud_usage_collector.ts} | 25 +-- x-pack/legacy/plugins/cloud/index.js | 5 +- x-pack/legacy/plugins/file_upload/index.js | 10 +- .../plugins/file_upload/server/plugin.js | 13 +- .../telemetry/file_upload_usage_collector.ts | 28 +++ .../file_upload/server/telemetry/index.ts | 2 +- .../plugins/infra/server/kibana.index.ts | 9 +- .../infra/server/usage/usage_collector.ts | 12 +- x-pack/legacy/plugins/lens/index.ts | 4 +- x-pack/legacy/plugins/lens/server/plugin.tsx | 27 +-- .../plugins/lens/server/usage/collectors.ts | 29 +-- .../legacy/plugins/lens/server/usage/task.ts | 6 +- x-pack/legacy/plugins/maps/index.js | 4 +- .../maps_telemetry/maps_usage_collector.js | 11 +- .../plugins/maps/server/test_utils/index.js | 6 - x-pack/legacy/plugins/ml/index.ts | 2 +- .../ml_telemetry/make_ml_usage_collector.ts | 15 +- .../plugins/ml/server/new_platform/plugin.ts | 18 +- x-pack/legacy/plugins/monitoring/index.js | 6 +- .../server/kibana_monitoring/bulk_uploader.js | 30 +-- .../collectors/get_kibana_usage_collector.js | 4 +- .../collectors/get_ops_stats_collector.js | 5 +- .../collectors/get_settings_collector.js | 4 +- .../kibana_monitoring/collectors/index.js | 14 +- .../server/kibana_monitoring/index.js | 1 + .../__test__/get_collection_status.js | 11 + .../setup/collection/get_collection_status.js | 16 +- .../plugins/monitoring/server/plugin.js | 22 +- .../__tests__/get_cluster_uuids.js | 21 +- .../legacy/plugins/oss_telemetry/index.d.ts | 6 - x-pack/legacy/plugins/oss_telemetry/index.js | 3 +- .../server/lib/collectors/index.ts | 5 +- .../register_usage_collector.ts | 11 +- .../plugins/oss_telemetry/test_utils/index.ts | 6 - x-pack/legacy/plugins/reporting/index.ts | 7 +- .../plugins/reporting/server/usage/index.ts | 2 +- ...t.js => reporting_usage_collector.test.js} | 32 +-- ...lector.ts => reporting_usage_collector.ts} | 19 +- x-pack/legacy/plugins/rollup/index.js | 3 +- .../plugins/rollup/server/usage/collector.js | 6 +- x-pack/legacy/plugins/spaces/index.ts | 1 - .../legacy/plugins/upgrade_assistant/index.ts | 3 +- .../lib/telemetry/es_ui_open_apis.test.ts | 6 - .../lib/telemetry/es_ui_reindex_apis.test.ts | 6 - .../server/np_ready/lib/telemetry/index.ts | 2 +- .../lib/telemetry/usage_collector.test.ts | 21 +- .../np_ready/lib/telemetry/usage_collector.ts | 10 +- .../server/np_ready/plugin.ts | 10 +- .../server/np_ready/types.ts | 6 - x-pack/legacy/plugins/uptime/index.ts | 4 +- .../plugins/uptime/server/kibana.index.ts | 10 +- .../lib/adapters/framework/adapter_types.ts | 3 +- .../kibana_telemetry_adapter.test.ts | 16 +- .../telemetry/kibana_telemetry_adapter.ts | 11 +- .../server/routes/api/v1/settings.js | 4 +- x-pack/plugins/spaces/kibana.json | 2 +- ...test.ts => spaces_usage_collector.test.ts} | 25 +-- ...collector.ts => spaces_usage_collector.ts} | 21 +- x-pack/plugins/spaces/server/plugin.ts | 36 +-- .../api/__fixtures__/create_legacy_api.ts | 1 - 112 files changed, 1046 insertions(+), 868 deletions(-) create mode 100644 src/legacy/core_plugins/telemetry/README.md delete mode 100644 src/legacy/server/usage/README.md delete mode 100644 src/legacy/server/usage/classes/collector_set.js create mode 100644 src/plugins/usage_collection/README.md rename src/{legacy/server/usage/lib/index.js => plugins/usage_collection/common/constants.ts} (92%) create mode 100644 src/plugins/usage_collection/kibana.json rename src/{legacy/server/usage/classes => plugins/usage_collection/server/collector}/__tests__/collector_set.js (85%) rename src/{legacy/server/usage/classes => plugins/usage_collection/server/collector}/collector.js (93%) create mode 100644 src/plugins/usage_collection/server/collector/collector_set.ts rename src/{legacy/server/usage/classes/index.js => plugins/usage_collection/server/collector/index.ts} (97%) rename src/{legacy/server/usage/classes => plugins/usage_collection/server/collector}/usage_collector.js (88%) rename src/{legacy/server/usage/lib/get_collector_logger.js => plugins/usage_collection/server/config.ts} (67%) rename src/{legacy/server/usage/index.js => plugins/usage_collection/server/index.ts} (63%) create mode 100644 src/plugins/usage_collection/server/plugin.ts rename x-pack/legacy/plugins/cloud/{get_cloud_usage_collector.test.ts => cloud_usage_collector.test.ts} (56%) rename x-pack/legacy/plugins/cloud/{get_cloud_usage_collector.ts => cloud_usage_collector.ts} (57%) create mode 100644 x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts rename x-pack/legacy/plugins/reporting/server/usage/{get_reporting_usage_collector.test.js => reporting_usage_collector.test.js} (93%) rename x-pack/legacy/plugins/reporting/server/usage/{get_reporting_usage_collector.ts => reporting_usage_collector.ts} (70%) rename x-pack/plugins/spaces/server/lib/{get_spaces_usage_collector.test.ts => spaces_usage_collector.test.ts} (83%) rename x-pack/plugins/spaces/server/lib/{get_spaces_usage_collector.ts => spaces_usage_collector.ts} (88%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e208dc73c7b4b..d567f267afa9d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -77,6 +77,7 @@ /src/dev/i18n @elastic/kibana-stack-services /packages/kbn-analytics/ @elastic/kibana-stack-services /src/legacy/core_plugins/ui_metric/ @elastic/kibana-stack-services +/src/plugins/usage_collection/ @elastic/kibana-stack-services /x-pack/legacy/plugins/telemetry @elastic/kibana-stack-services /x-pack/legacy/plugins/alerting @elastic/kibana-stack-services /x-pack/legacy/plugins/actions @elastic/kibana-stack-services diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 11a5f33a1b2d8..fbe2740b96108 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -210,3 +210,40 @@ export class Plugin { } } ``` + +### Usage Collection + +For creating and registering a Usage Collector. Collectors would be defined in a separate directory `server/collectors/register.ts`. You can read more about usage collectors on `src/plugins/usage_collection/README.md`. + +```ts +// server/collectors/register.ts +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + if (!usageCollection) { + return; + } + + // create usage collector + const myCollector = usageCollection.makeUsageCollector({ + type: MY_USAGE_TYPE, + fetch: async (callCluster: CallCluster) => { + + // query ES and get some data + // summarize the data into a model + // return the modeled object that includes whatever you want to track + + return { + my_objects: { + total: SOME_NUMBER + } + }; + }, + }); + + // register usage collector + usageCollection.registerCollector(myCollector); +} +``` diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index dcb7d7998ff1a..91364071579ab 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -324,6 +324,7 @@ export default function (kibana) { }, init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; // uuid await manageUuid(server); // routes @@ -338,8 +339,8 @@ export default function (kibana) { registerKqlTelemetryApi(server); registerFieldFormats(server); registerTutorials(server); - makeKQLUsageCollector(server); - registerCspCollector(server); + makeKQLUsageCollector(usageCollection, server); + registerCspCollector(usageCollection, server); server.expose('systemApi', systemApi); server.injectUiAppVars('kibana', () => injectVars(server)); }, diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts index 3ff39c1a4eb8c..9890aaf187a13 100644 --- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts +++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts @@ -19,6 +19,7 @@ import { Server } from 'hapi'; import { createCSPRuleString, DEFAULT_CSP_RULES } from '../../../../../server/csp'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; export function createCspCollector(server: Server) { return { @@ -42,8 +43,7 @@ export function createCspCollector(server: Server) { }; } -export function registerCspCollector(server: Server): void { - const { collectorSet } = server.usage; - const collector = collectorSet.makeUsageCollector(createCspCollector(server)); - collectorSet.register(collector); +export function registerCspCollector(usageCollection: UsageCollectionSetup, server: Server): void { + const collector = usageCollection.makeUsageCollector(createCspCollector(server)); + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js index 19fb64b7ecc74..6d751a9e9ff45 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js @@ -19,14 +19,14 @@ import { fetchProvider } from './fetch'; -export function makeKQLUsageCollector(server) { +export function makeKQLUsageCollector(usageCollection, server) { const index = server.config().get('kibana.index'); const fetch = fetchProvider(index); - const kqlUsageCollector = server.usage.collectorSet.makeUsageCollector({ + const kqlUsageCollector = usageCollection.makeUsageCollector({ type: 'kql', fetch, isReady: () => true, }); - server.usage.collectorSet.register(kqlUsageCollector); + usageCollection.registerCollector(kqlUsageCollector); } diff --git a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js index 24f336043d0d1..7737a0fbc2a71 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.test.js @@ -20,29 +20,30 @@ import { makeKQLUsageCollector } from './make_kql_usage_collector'; describe('makeKQLUsageCollector', () => { - let server; let makeUsageCollectorStub; let registerStub; + let usageCollection; beforeEach(() => { makeUsageCollectorStub = jest.fn(); registerStub = jest.fn(); + usageCollection = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + }; server = { - usage: { - collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub }, - }, config: () => ({ get: () => '.kibana' }) }; }); - it('should call collectorSet.register', () => { - makeKQLUsageCollector(server); + it('should call registerCollector', () => { + makeKQLUsageCollector(usageCollection, server); expect(registerStub).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = kql', () => { - makeKQLUsageCollector(server); + makeKQLUsageCollector(usageCollection, server); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('kql'); }); diff --git a/src/legacy/core_plugins/telemetry/README.md b/src/legacy/core_plugins/telemetry/README.md new file mode 100644 index 0000000000000..830c08f8e8bed --- /dev/null +++ b/src/legacy/core_plugins/telemetry/README.md @@ -0,0 +1,9 @@ +# Kibana Telemetry Service + +Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: + +1. Integrating with the telemetry service to express how to collect usage data (Collecting). +2. Sending a payload of usage data up to Elastic's telemetry cluster. +3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing). + +This plugin is responsible for sending usage data to the telemetry cluster. For collecting usage data, use diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 5ae0d5f127eed..9f850fc0fe719 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -27,14 +27,7 @@ import { i18n } from '@kbn/i18n'; import mappings from './mappings.json'; import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants'; import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated'; -import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask } from './server'; - -import { - createLocalizationUsageCollector, - createTelemetryUsageCollector, - createUiMetricUsageCollector, - createTelemetryPluginUsageCollector, -} from './server/collectors'; +import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask, PluginsSetup } from './server'; const ENDPOINT_VERSION = 'v2'; @@ -123,6 +116,7 @@ const telemetry = (kibana: any) => { fetcherTask.start(); }, init(server: Server) { + const { usageCollection } = server.newPlatform.setup.plugins; const initializerContext = { env: { packageInfo: { @@ -149,12 +143,11 @@ const telemetry = (kibana: any) => { log: server.log, } as any) as CoreSetup; - telemetryPlugin(initializerContext).setup(coreSetup); - // register collectors - server.usage.collectorSet.register(createTelemetryPluginUsageCollector(server)); - server.usage.collectorSet.register(createLocalizationUsageCollector(server)); - server.usage.collectorSet.register(createTelemetryUsageCollector(server)); - server.usage.collectorSet.register(createUiMetricUsageCollector(server)); + const pluginsSetup: PluginsSetup = { + usageCollection, + }; + + telemetryPlugin(initializerContext).setup(coreSetup, pluginsSetup, server); }, }); }; diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts index 799d9f4ee9c8b..933c249cd7279 100644 --- a/src/legacy/core_plugins/telemetry/server/collection_manager.ts +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -19,6 +19,7 @@ import { encryptTelemetry } from './collectors'; import { CallCluster } from '../../elasticsearch'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; export type EncryptedStatsGetterConfig = { unencrypted: false } & { server: any; @@ -37,6 +38,7 @@ export interface ClusterDetails { } export interface StatsCollectionConfig { + usageCollection: UsageCollectionSetup; callCluster: CallCluster; server: any; start: string; @@ -112,7 +114,8 @@ export class TelemetryCollectionManager { ? (...args: any[]) => callWithRequest(config.req, ...args) : callWithInternalUser; - return { server, callCluster, start, end }; + const { usageCollection } = server.newPlatform.setup.plugins; + return { server, callCluster, start, end, usageCollection }; }; private getOptInStatsForCollection = async ( diff --git a/src/legacy/core_plugins/telemetry/server/collectors/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/index.ts index f963ecec0477c..2f2a53278117b 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/index.ts @@ -18,7 +18,7 @@ */ export { encryptTelemetry } from './encryption'; -export { createTelemetryUsageCollector } from './usage'; -export { createUiMetricUsageCollector } from './ui_metric'; -export { createLocalizationUsageCollector } from './localization'; -export { createTelemetryPluginUsageCollector } from './telemetry_plugin'; +export { registerTelemetryUsageCollector } from './usage'; +export { registerUiMetricUsageCollector } from './ui_metric'; +export { registerLocalizationUsageCollector } from './localization'; +export { registerTelemetryPluginUsageCollector } from './telemetry_plugin'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts index 3b289752ce39f..71026b026263f 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createLocalizationUsageCollector } from './telemetry_localization_collector'; +export { registerLocalizationUsageCollector } from './telemetry_localization_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts index 74c93931096b2..191565187be14 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts @@ -21,6 +21,7 @@ import { i18nLoader } from '@kbn/i18n'; import { size } from 'lodash'; import { getIntegrityHashes, Integrities } from './file_integrity'; import { KIBANA_LOCALIZATION_STATS_TYPE } from '../../../common/constants'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; export interface UsageStats { locale: string; integrities: Integrities; @@ -51,15 +52,15 @@ export function createCollectorFetch(server: any) { }; } -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function createLocalizationUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerLocalizationUsageCollector( + usageCollection: UsageCollectionSetup, + server: any +) { + const collector = usageCollection.makeUsageCollector({ type: KIBANA_LOCALIZATION_STATS_TYPE, isReady: () => true, fetch: createCollectorFetch(server), }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts index e96c47741f79c..631a37e674c4e 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createTelemetryPluginUsageCollector } from './telemetry_plugin_collector'; +export { registerTelemetryPluginUsageCollector } from './telemetry_plugin_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts index a172ba7dc6955..5e25538cbad80 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts @@ -20,6 +20,8 @@ import { TELEMETRY_STATS_TYPE } from '../../../common/constants'; import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository'; import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../telemetry_config'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; + export interface TelemetryUsageStats { opt_in_status?: boolean | null; usage_fetcher?: 'browser' | 'server'; @@ -61,15 +63,15 @@ export function createCollectorFetch(server: any) { }; } -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function createTelemetryPluginUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerTelemetryPluginUsageCollector( + usageCollection: UsageCollectionSetup, + server: any +) { + const collector = usageCollection.makeUsageCollector({ type: TELEMETRY_STATS_TYPE, isReady: () => true, fetch: createCollectorFetch(server), }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts index e1ac7a1f5af12..013db526211e1 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createUiMetricUsageCollector } from './telemetry_ui_metric_collector'; +export { registerUiMetricUsageCollector } from './telemetry_ui_metric_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index fa3159669c33c..73157abce8629 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -18,10 +18,10 @@ */ import { UI_METRIC_USAGE_TYPE } from '../../../common/constants'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; -export function createUiMetricUsageCollector(server: any) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function registerUiMetricUsageCollector(usageCollection: UsageCollectionSetup, server: any) { + const collector = usageCollection.makeUsageCollector({ type: UI_METRIC_USAGE_TYPE, fetch: async () => { const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; @@ -55,4 +55,6 @@ export function createUiMetricUsageCollector(server: any) { }, isReady: () => true, }); + + usageCollection.registerCollector(collector); } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts index a1b3d5a7b1982..3ef9eed3c1265 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { createTelemetryUsageCollector } from './telemetry_usage_collector'; +export { registerTelemetryUsageCollector } from './telemetry_usage_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts index 3806dfc77120f..2b2e946198e0a 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts @@ -25,20 +25,15 @@ import { createTelemetryUsageCollector, isFileReadable, readTelemetryFile, - KibanaHapiServer, MAX_FILE_SIZE, } from './telemetry_usage_collector'; -const getMockServer = (): KibanaHapiServer => - ({ - usage: { - collectorSet: { makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg) }, - }, - } as KibanaHapiServer & Server); +const mockUsageCollector = () => ({ + makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg), +}); -const serverWithConfig = (configPath: string): KibanaHapiServer & Server => { +const serverWithConfig = (configPath: string): Server => { return { - ...getMockServer(), config: () => ({ get: (key: string) => { if (key !== 'telemetry.config' && key !== 'xpack.xpack_main.telemetry.config') { @@ -48,7 +43,7 @@ const serverWithConfig = (configPath: string): KibanaHapiServer & Server => { return configPath; }, }), - } as KibanaHapiServer & Server; + } as Server; }; describe('telemetry_usage_collector', () => { @@ -130,14 +125,15 @@ describe('telemetry_usage_collector', () => { }); describe('createTelemetryUsageCollector', () => { - test('calls `collectorSet.makeUsageCollector`', async () => { + test('calls `makeUsageCollector`', async () => { // note: it uses the file's path to get the directory, then looks for 'telemetry.yml' // exclusively, which is indirectly tested by passing it the wrong "file" in the same // dir - const server: KibanaHapiServer & Server = serverWithConfig(tempFiles.unreadable); + const server: Server = serverWithConfig(tempFiles.unreadable); // the `makeUsageCollector` is mocked above to return the argument passed to it - const collectorOptions = createTelemetryUsageCollector(server); + const usageCollector = mockUsageCollector() as any; + const collectorOptions = createTelemetryUsageCollector(usageCollector, server); expect(collectorOptions.type).toBe('static_telemetry'); expect(await collectorOptions.fetch()).toEqual(expectedObject); diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index c927453641193..99090cb2fb7ef 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -25,20 +25,13 @@ import { dirname, join } from 'path'; // look for telemetry.yml in the same places we expect kibana.yml import { ensureDeepObject } from './ensure_deep_object'; import { getXpackConfigWithDeprecated } from '../../../common/get_xpack_config_with_deprecated'; +import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; /** * The maximum file size before we ignore it (note: this limit is arbitrary). */ export const MAX_FILE_SIZE = 10 * 1024; // 10 KB -export interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: (collector: object) => any; - }; - }; -} - /** * Determine if the supplied `path` is readable. * @@ -83,19 +76,11 @@ export async function readTelemetryFile(path: string): Promise true, fetch: async () => { @@ -106,3 +91,11 @@ export function createTelemetryUsageCollector(server: KibanaHapiServer) { }, }); } + +export function registerTelemetryUsageCollector( + usageCollection: UsageCollectionSetup, + server: Server +) { + const collector = createTelemetryUsageCollector(usageCollection, server); + usageCollection.registerCollector(collector); +} diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts index 02752ca773488..6c62d03adf25c 100644 --- a/src/legacy/core_plugins/telemetry/server/index.ts +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -24,7 +24,7 @@ import * as constants from '../common/constants'; export { FetcherTask } from './fetcher'; export { replaceTelemetryInjectedVars } from './telemetry_config'; export { telemetryCollectionManager } from './collection_manager'; - +export { PluginsSetup } from './plugin'; export const telemetryPlugin = (initializerContext: PluginInitializerContext) => new TelemetryPlugin(initializerContext); export { constants }; diff --git a/src/legacy/core_plugins/telemetry/server/plugin.ts b/src/legacy/core_plugins/telemetry/server/plugin.ts index f2628090c08af..06a974f473498 100644 --- a/src/legacy/core_plugins/telemetry/server/plugin.ts +++ b/src/legacy/core_plugins/telemetry/server/plugin.ts @@ -18,8 +18,20 @@ */ import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { Server } from 'hapi'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; +import { + registerUiMetricUsageCollector, + registerTelemetryUsageCollector, + registerLocalizationUsageCollector, + registerTelemetryPluginUsageCollector, +} from './collectors'; + +export interface PluginsSetup { + usageCollection: UsageCollectionSetup; +} export class TelemetryPlugin { private readonly currentKibanaVersion: string; @@ -28,9 +40,15 @@ export class TelemetryPlugin { this.currentKibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { usageCollection }: PluginsSetup, server: Server) { const currentKibanaVersion = this.currentKibanaVersion; + registerCollection(); registerRoutes({ core, currentKibanaVersion }); + + registerTelemetryPluginUsageCollector(usageCollection, server); + registerLocalizationUsageCollector(usageCollection, server); + registerTelemetryUsageCollector(usageCollection, server); + registerUiMetricUsageCollector(usageCollection, server); } } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index 4cbdf18df4a74..140204ac5ab49 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -29,7 +29,12 @@ import { handleLocalStats, } from '../get_local_stats'; -const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({ +const mockUsageCollection = (kibanaUsage = {}) => ({ + bulkFetch: () => kibanaUsage, + toObject: data => data, +}); + +const getMockServer = (getCluster = sinon.stub()) => ({ log(tags, message) { console.log({ tags, message }); }, @@ -43,7 +48,6 @@ const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({ } }; }, - usage: { collectorSet: { bulkFetch: () => kibanaUsage, toObject: data => data } }, plugins: { elasticsearch: { getCluster }, }, @@ -155,15 +159,16 @@ describe('get_local_stats', () => { describe.skip('getLocalStats', () => { it('returns expected object without xpack data when X-Pack fails to respond', async () => { const callClusterUsageFailed = sinon.stub(); - + const usageCollection = mockUsageCollection(); mockGetLocalStats( callClusterUsageFailed, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), ); - const result = await getLocalStats({ + const result = await getLocalStats([], { server: getMockServer(), callCluster: callClusterUsageFailed, + usageCollection, }); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); @@ -178,15 +183,16 @@ describe('get_local_stats', () => { it('returns expected object with xpack and kibana data', async () => { const callCluster = sinon.stub(); - + const usageCollection = mockUsageCollection(kibana); mockGetLocalStats( callCluster, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), ); - const result = await getLocalStats({ - server: getMockServer(callCluster, kibana), + const result = await getLocalStats([], { + server: getMockServer(callCluster), + usageCollection, callCluster, }); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js index 051ef370fcde5..236dd046148f6 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js @@ -47,12 +47,7 @@ export function handleKibanaStats(server, response) { }; } -/* - * Check user privileges for read access to monitoring - * Pass callWithInternalUser to bulkFetchUsage - */ -export async function getKibana(server, callWithInternalUser) { - const { collectorSet } = server.usage; - const usage = await collectorSet.bulkFetch(callWithInternalUser); - return collectorSet.toObject(usage); +export async function getKibana(usageCollection, callWithInternalUser) { + const usage = await usageCollection.bulkFetch(callWithInternalUser); + return usageCollection.toObject(usage); } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts index e11c6b1277d5b..a4ea2eb534226 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -55,13 +55,14 @@ export function handleLocalStats(server: any, clusterInfo: any, clusterStats: an * @return {Promise} The object containing the current Elasticsearch cluster's telemetry. */ export const getLocalStats: StatsGetter = async (clustersDetails, config) => { - const { server, callCluster } = config; + const { server, callCluster, usageCollection } = config; + return await Promise.all( clustersDetails.map(async clustersDetail => { const [clusterInfo, clusterStats, kibana] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) - getKibana(server, callCluster), + getKibana(usageCollection, callCluster), ]); return handleLocalStats(server, clusterInfo, clusterStats, kibana); }) diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 9cc4e30d4252d..6f2730476956e 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -38,7 +38,7 @@ import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from '../../core/serve import { SavedObjectsManagement } from '../../core/server/saved_objects/management'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; - +import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; import { CapabilitiesModifier } from './capabilities'; import { IndexPatternsServiceFactory } from './index_patterns'; import { Capabilities } from '../../core/public'; @@ -67,7 +67,6 @@ declare module 'hapi' { config: () => KibanaConfig; indexPatternsServiceFactory: IndexPatternsServiceFactory; savedObjects: SavedObjectsLegacyService; - usage: { collectorSet: any }; injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void; getHiddenUiAppById(appId: string): UiApp; registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void; @@ -101,6 +100,11 @@ declare module 'hapi' { type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; +export interface PluginsSetup { + usageCollection: UsageCollectionSetup; + [key: string]: object; +} + // eslint-disable-next-line import/no-default-export export default class KbnServer { public readonly newPlatform: { @@ -120,7 +124,7 @@ export default class KbnServer { }; setup: { core: CoreSetup; - plugins: Record; + plugins: PluginsSetup; }; start: { core: CoreSetup; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index f7ed56b10c267..e5f182c931d80 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -28,7 +28,6 @@ import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; import warningsMixin from './warnings'; -import { usageMixin } from './usage'; import { statusMixin } from './status'; import pidMixin from './pid'; import { configDeprecationWarningsMixin } from './config/deprecation_warnings'; @@ -94,7 +93,6 @@ export default class KbnServer { loggingMixin, configDeprecationWarningsMixin, warningsMixin, - usageMixin, statusMixin, // writes pid file diff --git a/src/legacy/server/sample_data/usage/collector.ts b/src/legacy/server/sample_data/usage/collector.ts index 8561a6c3f1007..bcb5e7be2597a 100644 --- a/src/legacy/server/sample_data/usage/collector.ts +++ b/src/legacy/server/sample_data/usage/collector.ts @@ -17,26 +17,25 @@ * under the License. */ -import * as Hapi from 'hapi'; +import { Server } from 'hapi'; import { fetchProvider } from './collector_fetch'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server'; -interface KbnServer extends Hapi.Server { - usage: any; -} - -export function makeSampleDataUsageCollector(server: KbnServer) { +export function makeSampleDataUsageCollector( + usageCollection: UsageCollectionSetup, + server: Server +) { let index: string; try { index = server.config().get('kibana.index'); } catch (err) { return; // kibana plugin is not enabled (test environment) } + const collector = usageCollection.makeUsageCollector({ + type: 'sample-data', + fetch: fetchProvider(index), + isReady: () => true, + }); - server.usage.collectorSet.register( - server.usage.collectorSet.makeUsageCollector({ - type: 'sample-data', - fetch: fetchProvider(index), - isReady: () => true, - }) - ); + usageCollection.registerCollector(collector); } diff --git a/src/legacy/server/status/collectors/get_ops_stats_collector.js b/src/legacy/server/status/collectors/get_ops_stats_collector.js index aded85384fd85..116e588c5ade6 100644 --- a/src/legacy/server/status/collectors/get_ops_stats_collector.js +++ b/src/legacy/server/status/collectors/get_ops_stats_collector.js @@ -35,9 +35,8 @@ import { getKibanaInfoForStats } from '../lib'; * the metrics. * See PR comment in https://github.com/elastic/kibana/pull/20577/files#r202416647 */ -export function getOpsStatsCollector(server, kbnServer) { - const { collectorSet } = server.usage; - return collectorSet.makeStatsCollector({ +export function getOpsStatsCollector(usageCollection, server, kbnServer) { + return usageCollection.makeStatsCollector({ type: KIBANA_STATS_TYPE, fetch: () => { return { @@ -49,3 +48,10 @@ export function getOpsStatsCollector(server, kbnServer) { ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there. }); } + +export function registerOpsStatsCollector(usageCollection, server, kbnServer) { + if (usageCollection) { + const collector = getOpsStatsCollector(usageCollection, server, kbnServer); + usageCollection.registerCollector(collector); + } +} diff --git a/src/legacy/server/status/collectors/index.js b/src/legacy/server/status/collectors/index.js index 4310dff7359ef..92d9e601bbb35 100644 --- a/src/legacy/server/status/collectors/index.js +++ b/src/legacy/server/status/collectors/index.js @@ -17,4 +17,4 @@ * under the License. */ -export { getOpsStatsCollector } from './get_ops_stats_collector'; +export { registerOpsStatsCollector } from './get_ops_stats_collector'; diff --git a/src/legacy/server/status/index.js b/src/legacy/server/status/index.js index dda20878605e5..ba2f835599bc9 100644 --- a/src/legacy/server/status/index.js +++ b/src/legacy/server/status/index.js @@ -20,17 +20,15 @@ import ServerStatus from './server_status'; import { Metrics } from './lib/metrics'; import { registerStatusPage, registerStatusApi, registerStatsApi } from './routes'; -import { getOpsStatsCollector } from './collectors'; +import { registerOpsStatsCollector } from './collectors'; import Oppsy from 'oppsy'; import { cloneDeep } from 'lodash'; import { getOSInfo } from './lib/get_os_info'; export function statusMixin(kbnServer, server, config) { kbnServer.status = new ServerStatus(kbnServer.server); - - const statsCollector = getOpsStatsCollector(server, kbnServer); - const { collectorSet } = server.usage; - collectorSet.register(statsCollector); + const { usageCollection } = server.newPlatform.setup.plugins; + registerOpsStatsCollector(usageCollection, server, kbnServer); const metrics = new Metrics(config, server); @@ -57,7 +55,7 @@ export function statusMixin(kbnServer, server, config) { // init routes registerStatusPage(kbnServer, server, config); registerStatusApi(kbnServer, server, config); - registerStatsApi(kbnServer, server, config); + registerStatsApi(usageCollection, server, config); // expore shared functionality server.decorate('server', 'getOSInfo', getOSInfo); diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 91272ead1d2c1..366d36860731c 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -29,7 +29,7 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { /* * API for Kibana meta info and accumulated operations stats - * Including ?extended in the query string fetches Elasticsearch cluster_uuid and server.usage.collectorSet data + * Including ?extended in the query string fetches Elasticsearch cluster_uuid and usageCollection data * - Requests to set isExtended = true * GET /api/stats?extended=true * GET /api/stats?extended @@ -37,9 +37,8 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { * - Any other value causes a statusCode 400 response (Bad Request) * Including ?exclude_usage in the query string excludes the usage stats from the response. Same value semantics as ?extended */ -export function registerStatsApi(kbnServer, server, config) { +export function registerStatsApi(usageCollection, server, config) { const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); - const { collectorSet } = server.usage; const getClusterUuid = async callCluster => { const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid', }); @@ -47,8 +46,8 @@ export function registerStatsApi(kbnServer, server, config) { }; const getUsage = async callCluster => { - const usage = await collectorSet.bulkFetchUsage(callCluster); - return collectorSet.toObject(usage); + const usage = await usageCollection.bulkFetchUsage(callCluster); + return usageCollection.toObject(usage); }; server.route( @@ -74,7 +73,7 @@ export function registerStatsApi(kbnServer, server, config) { if (isExtended) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); const callCluster = (...args) => callWithRequest(req, ...args); - const collectorsReady = await collectorSet.areAllCollectorsReady(); + const collectorsReady = await usageCollection.areAllCollectorsReady(); if (shouldGetUsage && !collectorsReady) { return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); @@ -126,7 +125,7 @@ export function registerStatsApi(kbnServer, server, config) { }; } else { - extended = collectorSet.toApiFieldNames({ + extended = usageCollection.toApiFieldNames({ usage: modifiedUsage, clusterUuid }); @@ -139,12 +138,12 @@ export function registerStatsApi(kbnServer, server, config) { /* kibana_stats gets singled out from the collector set as it is used * for health-checking Kibana and fetch does not rely on fetching data * from ES */ - const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE); + const kibanaStatsCollector = usageCollection.getCollectorByType(KIBANA_STATS_TYPE); if (!await kibanaStatsCollector.isReady()) { return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); } let kibanaStats = await kibanaStatsCollector.fetch(); - kibanaStats = collectorSet.toApiFieldNames(kibanaStats); + kibanaStats = usageCollection.toApiFieldNames(kibanaStats); return { ...kibanaStats, diff --git a/src/legacy/server/usage/README.md b/src/legacy/server/usage/README.md deleted file mode 100644 index 5c4bcc05bbc38..0000000000000 --- a/src/legacy/server/usage/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Kibana Telemetry Service - -Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: - -1. Integrating with the telemetry service to express how to collect usage data (Collecting). -2. Sending a payload of usage data up to Elastic's telemetry cluster, once per browser per day (Sending). -3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing). - -You, the feature or plugin developer, mainly need to worry about the first meaning: collecting. To integrate with the telemetry services for usage collection of your feature, there are 2 steps: - -1. Create a usage collector using a factory function -2. Register the usage collector with the Telemetry service - -NOTE: To a lesser extent, there's also a need to update the telemetry payload of Kibana stats and telemetry cluster field mappings to include your fields. This part is typically handled not by you, the developer, but different maintainers of the telemetry cluster. Usually, this step just means talk to the Platform team and have them approve your data model or added fields. - -## Creating and Registering Usage Collector - -A usage collector object is an instance of a class called `UsageCollector`. A factory function on `server.usage.collectorSet` object allows you to create an instance of this class. All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it. - -Example: - -```js -// create usage collector -const myCollector = server.usage.collectorSet.makeUsageCollector({ - type: MY_USAGE_TYPE, - fetch: async callCluster => { - - // query ES and get some data - // summarize the data into a model - // return the modeled object that includes whatever you want to track - - return { - my_objects: { - total: SOME_NUMBER - } - }; - }, -}); - -// register usage collector -server.usage.collectorSet.register(myCollector); -``` - -Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection. - -The fetch method also might be called through an internal background task on the Kibana server, which currently lives in the `kibana_monitoring` module of the X-Pack Monitoring plugin, that polls for data and uploads it to Elasticsearch through a bulk API exposed by the Monitoring plugin for Elasticsearch. In this case, the `callCluster` method will be the internal system user and will have read privilege over the entire `.kibana` index. - -Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. - - -Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`. - -## Update the telemetry payload and telemetry cluster field mappings - -There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. - -As of the time of this writing (pre-6.5.0) there are a few unpleasant realities with this module. Today, this module has to be aware of all the features that have integrated with it, which it does from hard-coding. It does this because at the time of creation, the payload implemented a designed model where X-Pack plugin info went together regardless if it was ES-specific or Kibana-specific. In hindsight, all the Kibana data could just be put together, X-Pack or not, which it could do in a generic way. This is a known problem and a solution will be implemented in an upcoming refactoring phase, as this would break the contract for model of data sent in the payload. - -The second reality is that new fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in Kibana visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster. - -## Testing - -There are a few ways you can test that your usage collector is working properly. - -1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. -2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: - - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. - - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. ✳ - - The dev script in x-pack can be run on the command-line with: - ``` - cd x-pack - node scripts/api_debug.js telemetry --host=http://localhost:5601 - ``` - Where `http://localhost:5601` is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well. - - Automatic inclusion of all the stats fetched by collectors is added in https://github.com/elastic/kibana/pull/22336 / 6.5.0 -3. In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields. -4. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload. - -✳ At the time of this writing, there is an open issue that in the sending phase, Kibana usage collectors are not "live-pulled" from Kibana API endpoints if Monitoring is disabled. The implementation on this depends on a new secure way to live-pull the data from the end-user's browser, as it would not be appropriate to supply only partial data if the logged-in user only has partial access to `.kibana`. - -## FAQ - -1. **Can telemetry track UI interactions, such as button click?** - Brief answer: no. Telemetry collection happens on the server-side so the usage data will only include information that the server-side is aware of. There is no generic way to do this today, but UI-interaction KPIs can be tracked with a custom server endpoint that gets called for tracking when the UI event happens. -2. **Does the telemetry service have a hook that I can call whenever some event happens in my feature?** - Brief answer: no. Telemetry collection is a fetch model, not a push model. Telemetry fetches info from your collector. -3. **How should I design my data model?** - Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine. -4. **Can the telemetry payload include dynamic fields?** - Yes. When you talk to the Platform team about new fields being added, point out specifically which properties will have dynamic inner fields. -5. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?** - Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that. diff --git a/src/legacy/server/usage/classes/collector_set.js b/src/legacy/server/usage/classes/collector_set.js deleted file mode 100644 index 5a86992f0af71..0000000000000 --- a/src/legacy/server/usage/classes/collector_set.js +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { snakeCase } from 'lodash'; -import { getCollectorLogger } from '../lib'; -import { Collector } from './collector'; -import { UsageCollector } from './usage_collector'; - -let _waitingForAllCollectorsTimestamp = null; - -/* - * A collector object has types registered into it with the register(type) - * function. Each type that gets registered defines how to fetch its own data - * and optionally, how to combine it into a unified payload for bulk upload. - */ -export class CollectorSet { - /* - * @param {Object} server - server object - * @param {Array} collectors to initialize, usually as a result of filtering another CollectorSet instance - */ - constructor(server, collectors = [], config = null) { - this._log = getCollectorLogger(server); - this._collectors = collectors; - - /* - * Helper Factory methods - * Define as instance properties to allow enclosing the server object - */ - this.makeStatsCollector = options => new Collector(server, options); - this.makeUsageCollector = options => new UsageCollector(server, options); - this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray, config); - - this._maximumWaitTimeForAllCollectorsInS = config ? config.get('stats.maximumWaitTimeForAllCollectorsInS') : 60; - } - - /* - * @param collector {Collector} collector object - */ - register(collector) { - // check instanceof - if (!(collector instanceof Collector)) { - throw new Error('CollectorSet can only have Collector instances registered'); - } - - this._collectors.push(collector); - - if (collector.init) { - this._log.debug(`Initializing ${collector.type} collector`); - collector.init(); - } - } - - getCollectorByType(type) { - return this._collectors.find(c => c.type === type); - } - - // isUsageCollector(x: UsageCollector | any): x is UsageCollector { - isUsageCollector(x) { - return x instanceof UsageCollector; - } - - async areAllCollectorsReady(collectorSet = this) { - if (!(collectorSet instanceof CollectorSet)) { - throw new Error(`areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet); - } - - const collectorTypesNotReady = []; - let allReady = true; - await collectorSet.asyncEach(async collector => { - if (!await collector.isReady()) { - allReady = false; - collectorTypesNotReady.push(collector.type); - } - }); - - if (!allReady && this._maximumWaitTimeForAllCollectorsInS >= 0) { - const nowTimestamp = +new Date(); - _waitingForAllCollectorsTimestamp = _waitingForAllCollectorsTimestamp || nowTimestamp; - const timeWaitedInMS = nowTimestamp - _waitingForAllCollectorsTimestamp; - const timeLeftInMS = (this._maximumWaitTimeForAllCollectorsInS * 1000) - timeWaitedInMS; - if (timeLeftInMS <= 0) { - this._log.debug(`All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` - + `but we have waited the required ` - + `${this._maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.`); - return true; - } else { - this._log.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`); - } - } else { - _waitingForAllCollectorsTimestamp = null; - } - - return allReady; - } - - /* - * Call a bunch of fetch methods and then do them in bulk - * @param {CollectorSet} collectorSet - a set of collectors to fetch. Default to all registered collectors - */ - async bulkFetch(callCluster, collectorSet = this) { - if (!(collectorSet instanceof CollectorSet)) { - throw new Error(`bulkFetch method given bad collectorSet parameter: ` + typeof collectorSet); - } - - const responses = []; - await collectorSet.asyncEach(async collector => { - this._log.debug(`Fetching data from ${collector.type} collector`); - try { - responses.push({ - type: collector.type, - result: await collector.fetchInternal(callCluster) - }); - } - catch (err) { - this._log.warn(err); - this._log.warn(`Unable to fetch data from ${collector.type} collector`); - } - }); - return responses; - } - - /* - * @return {new CollectorSet} - */ - getFilteredCollectorSet(filter) { - const filtered = this._collectors.filter(filter); - return this._makeCollectorSetFromArray(filtered); - } - - async bulkFetchUsage(callCluster) { - const usageCollectors = this.getFilteredCollectorSet(c => c instanceof UsageCollector); - return this.bulkFetch(callCluster, usageCollectors); - } - - // convert an array of fetched stats results into key/object - toObject(statsData) { - if (!statsData) return {}; - return statsData.reduce((accumulatedStats, { type, result }) => { - return { - ...accumulatedStats, - [type]: result, - }; - }, {}); - } - - // rename fields to use api conventions - toApiFieldNames(apiData) { - const getValueOrRecurse = value => { - if (value == null || typeof value !== 'object') { - return value; - } else { - return this.toApiFieldNames(value); // recurse - } - }; - - // handle array and return early, or return a reduced object - - if (Array.isArray(apiData)) { - return apiData.map(getValueOrRecurse); - } - - return Object.keys(apiData).reduce((accum, field) => { - const value = apiData[field]; - let newName = field; - newName = snakeCase(newName); - newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m - newName = newName.replace('_in_bytes', '_bytes'); - newName = newName.replace('_in_millis', '_ms'); - - return { - ...accum, - [newName]: getValueOrRecurse(value), - }; - }, {}); - } - - map(mapFn) { - return this._collectors.map(mapFn); - } - - some(someFn) { - return this._collectors.some(someFn); - } - - async asyncEach(eachFn) { - for (const collector of this._collectors) { - await eachFn(collector); - } - } -} diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md new file mode 100644 index 0000000000000..4502e1a6ceacf --- /dev/null +++ b/src/plugins/usage_collection/README.md @@ -0,0 +1,139 @@ +# Kibana Usage Collection Service + +Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). +To integrate with the telemetry services for usage collection of your feature, there are 2 steps: + +1. Create a usage collector. +2. Register the usage collector. + +## Creating and Registering Usage Collector + +All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it. + +### New Platform: + +1. Make sure `usageCollection` is in your optional Plugins: + +```json +// plugin/kibana.json +{ + "id": "...", + "optionalPlugins": ["usageCollection"] +} +``` + +2. Register Usage collector in the `setup` function: + +```ts +// server/plugin.ts +class Plugin { + setup(core, plugins) { + registerMyPluginUsageCollector(plugins.usageCollection); + } +} +``` + +3. Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory `server/collectors/register.ts`. + +```ts +// server/collectors/register.ts +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + if (!usageCollection) { + return; + } + + // create usage collector + const myCollector = usageCollection.makeUsageCollector({ + type: MY_USAGE_TYPE, + fetch: async (callCluster: CallCluster) => { + + // query ES and get some data + // summarize the data into a model + // return the modeled object that includes whatever you want to track + + return { + my_objects: { + total: SOME_NUMBER + } + }; + }, + }); + + // register usage collector + usageCollection.registerCollector(myCollector); +} +``` + +Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection. + +Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. + +### Migrating to NP from Legacy Plugins: + +Pass `usageCollection` to the setup NP plugin setup function under plugins. Inside the `setup` function call the `registerCollector` like what you'd do in the NP example above. + +```js +// index.js +export const myPlugin = (kibana: any) => { + return new kibana.Plugin({ + init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; + const plugins = { + usageCollection, + }; + plugin(initializerContext).setup(core, plugins); + } + }); +} +``` + +### Legacy Plugins: + +Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`. + +```js +// index.js +export const myPlugin = (kibana: any) => { + return new kibana.Plugin({ + init: async function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; + registerMyPluginUsageCollector(usageCollection); + } + }); +} +``` + +## Update the telemetry payload and telemetry cluster field mappings + +There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. + +New fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in Kibana visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster. + +## Testing + +There are a few ways you can test that your usage collector is working properly. + +1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`. +2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from: + - The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes. + - Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. + - The dev script in x-pack can be run on the command-line with: + ``` + cd x-pack + node scripts/api_debug.js telemetry --host=http://localhost:5601 + ``` + Where `http://localhost:5601` is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well. + - Automatic inclusion of all the stats fetched by collectors is added in https://github.com/elastic/kibana/pull/22336 / 6.5.0 +3. In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields. +4. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload. + +## FAQ + +1. **How should I design my data model?** + Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine. +2. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?** + Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that. diff --git a/src/legacy/server/usage/lib/index.js b/src/plugins/usage_collection/common/constants.ts similarity index 92% rename from src/legacy/server/usage/lib/index.js rename to src/plugins/usage_collection/common/constants.ts index 7db3cd4506503..edd06b171a72c 100644 --- a/src/legacy/server/usage/lib/index.js +++ b/src/plugins/usage_collection/common/constants.ts @@ -17,4 +17,4 @@ * under the License. */ -export { getCollectorLogger } from './get_collector_logger'; +export const KIBANA_STATS_TYPE = 'kibana_stats'; diff --git a/src/plugins/usage_collection/kibana.json b/src/plugins/usage_collection/kibana.json new file mode 100644 index 0000000000000..145cd89ff884d --- /dev/null +++ b/src/plugins/usage_collection/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "usageCollection", + "configPath": ["usageCollection"], + "version": "kibana", + "server": true, + "ui": false +} diff --git a/src/legacy/server/usage/classes/__tests__/collector_set.js b/src/plugins/usage_collection/server/collector/__tests__/collector_set.js similarity index 85% rename from src/legacy/server/usage/classes/__tests__/collector_set.js rename to src/plugins/usage_collection/server/collector/__tests__/collector_set.js index 5cf18a8a15200..a2e400b876ff7 100644 --- a/src/legacy/server/usage/classes/__tests__/collector_set.js +++ b/src/plugins/usage_collection/server/collector/__tests__/collector_set.js @@ -24,22 +24,25 @@ import { Collector } from '../collector'; import { CollectorSet } from '../collector_set'; import { UsageCollector } from '../usage_collector'; +const mockLogger = () => ({ + debug: sinon.spy(), + warn: sinon.spy(), +}); + describe('CollectorSet', () => { describe('registers a collector set and runs lifecycle events', () => { - let server; let init; let fetch; - beforeEach(() => { - server = { log: sinon.spy() }; init = noop; fetch = noop; }); it('should throw an error if non-Collector type of object is registered', () => { - const collectors = new CollectorSet(server); + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); const registerPojo = () => { - collectors.register({ + collectors.registerCollector({ type: 'type_collector_test', init, fetch, @@ -53,17 +56,17 @@ describe('CollectorSet', () => { it('should log debug status of fetching from the collector', async () => { const mockCallCluster = () => Promise.resolve({ passTest: 1000 }); - const collectors = new CollectorSet(server); - collectors.register(new Collector(server, { + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + collectors.registerCollector(new Collector(logger, { type: 'MY_TEST_COLLECTOR', fetch: caller => caller() })); const result = await collectors.bulkFetch(mockCallCluster); - const calls = server.log.getCalls(); + const calls = logger.debug.getCalls(); expect(calls.length).to.be(1); expect(calls[0].args).to.eql([ - ['debug', 'stats-collection'], 'Fetching data from MY_TEST_COLLECTOR collector', ]); expect(result).to.eql([{ @@ -74,8 +77,9 @@ describe('CollectorSet', () => { it('should gracefully handle a collector fetch method throwing an error', async () => { const mockCallCluster = () => Promise.resolve({ passTest: 1000 }); - const collectors = new CollectorSet(server); - collectors.register(new Collector(server, { + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + collectors.registerCollector(new Collector(logger, { type: 'MY_TEST_COLLECTOR', fetch: () => new Promise((_resolve, reject) => reject()) })); @@ -95,7 +99,8 @@ describe('CollectorSet', () => { let collectorSet; beforeEach(() => { - collectorSet = new CollectorSet(); + const logger = mockLogger(); + collectorSet = new CollectorSet({ logger }); }); it('should snake_case and convert field names to api standards', () => { @@ -161,14 +166,13 @@ describe('CollectorSet', () => { }); describe('isUsageCollector', () => { - const server = { }; const collectorOptions = { type: 'MY_TEST_COLLECTOR', fetch: () => {} }; it('returns true only for UsageCollector instances', () => { - const collectors = new CollectorSet(server); - - const usageCollector = new UsageCollector(server, collectorOptions); - const collector = new Collector(server, collectorOptions); + const logger = mockLogger(); + const collectors = new CollectorSet({ logger }); + const usageCollector = new UsageCollector(logger, collectorOptions); + const collector = new Collector(logger, collectorOptions); const randomClass = new (class Random {}); expect(collectors.isUsageCollector(usageCollector)).to.be(true); expect(collectors.isUsageCollector(collector)).to.be(false); diff --git a/src/legacy/server/usage/classes/collector.js b/src/plugins/usage_collection/server/collector/collector.js similarity index 93% rename from src/legacy/server/usage/classes/collector.js rename to src/plugins/usage_collection/server/collector/collector.js index 40b004f51e49a..ab723edf5b719 100644 --- a/src/legacy/server/usage/classes/collector.js +++ b/src/plugins/usage_collection/server/collector/collector.js @@ -17,18 +17,17 @@ * under the License. */ -import { getCollectorLogger } from '../lib'; export class Collector { /* - * @param {Object} server - server object + * @param {Object} logger - logger object * @param {String} options.type - property name as the key for the data * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data * @param {Function} options.formatForBulkUpload - optional * @param {Function} options.rest - optional other properties */ - constructor(server, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) { + constructor(logger, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); } @@ -39,7 +38,7 @@ export class Collector { throw new Error('Collector must be instantiated with a options.fetch function property'); } - this.log = getCollectorLogger(server); + this.log = logger; Object.assign(this, options); // spread in other properties and mutate "this" diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts new file mode 100644 index 0000000000000..a87accc47535e --- /dev/null +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -0,0 +1,209 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { snakeCase } from 'lodash'; +import { Logger } from 'kibana/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +// @ts-ignore +import { Collector } from './collector'; +// @ts-ignore +import { UsageCollector } from './usage_collector'; + +interface CollectorSetConfig { + logger: Logger; + maximumWaitTimeForAllCollectorsInS: number; + collectors?: Collector[]; +} + +export class CollectorSet { + private _waitingForAllCollectorsTimestamp?: number; + private logger: Logger; + private readonly maximumWaitTimeForAllCollectorsInS: number; + private collectors: Collector[] = []; + constructor({ logger, maximumWaitTimeForAllCollectorsInS, collectors = [] }: CollectorSetConfig) { + this.logger = logger; + this.collectors = collectors; + this.maximumWaitTimeForAllCollectorsInS = maximumWaitTimeForAllCollectorsInS || 60; + } + + public makeStatsCollector = (options: any) => { + return new Collector(this.logger, options); + }; + public makeUsageCollector = (options: any) => { + return new UsageCollector(this.logger, options); + }; + + /* + * @param collector {Collector} collector object + */ + public registerCollector = (collector: Collector) => { + // check instanceof + if (!(collector instanceof Collector)) { + throw new Error('CollectorSet can only have Collector instances registered'); + } + + this.collectors.push(collector); + + if (collector.init) { + this.logger.debug(`Initializing ${collector.type} collector`); + collector.init(); + } + }; + + public getCollectorByType = (type: string) => { + return this.collectors.find(c => c.type === type); + }; + + public isUsageCollector = (x: UsageCollector | any): x is UsageCollector => { + return x instanceof UsageCollector; + }; + + public areAllCollectorsReady = async (collectorSet = this) => { + if (!(collectorSet instanceof CollectorSet)) { + throw new Error( + `areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet + ); + } + + const collectorTypesNotReady: string[] = []; + let allReady = true; + for (const collector of collectorSet.collectors) { + if (!(await collector.isReady())) { + allReady = false; + collectorTypesNotReady.push(collector.type); + } + } + + if (!allReady && this.maximumWaitTimeForAllCollectorsInS >= 0) { + const nowTimestamp = +new Date(); + this._waitingForAllCollectorsTimestamp = + this._waitingForAllCollectorsTimestamp || nowTimestamp; + const timeWaitedInMS = nowTimestamp - this._waitingForAllCollectorsTimestamp; + const timeLeftInMS = this.maximumWaitTimeForAllCollectorsInS * 1000 - timeWaitedInMS; + if (timeLeftInMS <= 0) { + this.logger.debug( + `All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` + + `but we have waited the required ` + + `${this.maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.` + ); + return true; + } else { + this.logger.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`); + } + } else { + this._waitingForAllCollectorsTimestamp = undefined; + } + + return allReady; + }; + + public bulkFetch = async ( + callCluster: CallCluster, + collectors: Collector[] = this.collectors + ) => { + const responses = []; + for (const collector of collectors) { + this.logger.debug(`Fetching data from ${collector.type} collector`); + try { + responses.push({ + type: collector.type, + result: await collector.fetchInternal(callCluster), + }); + } catch (err) { + this.logger.warn(err); + this.logger.warn(`Unable to fetch data from ${collector.type} collector`); + } + } + + return responses; + }; + + /* + * @return {new CollectorSet} + */ + public getFilteredCollectorSet = (filter: any) => { + const filtered = this.collectors.filter(filter); + return this.makeCollectorSetFromArray(filtered); + }; + + public bulkFetchUsage = async (callCluster: CallCluster) => { + const usageCollectors = this.getFilteredCollectorSet((c: any) => c instanceof UsageCollector); + return await this.bulkFetch(callCluster, usageCollectors.collectors); + }; + + // convert an array of fetched stats results into key/object + public toObject = (statsData: any) => { + if (!statsData) return {}; + return statsData.reduce((accumulatedStats: any, { type, result }: any) => { + return { + ...accumulatedStats, + [type]: result, + }; + }, {}); + }; + + // rename fields to use api conventions + public toApiFieldNames = (apiData: any): any => { + const getValueOrRecurse = (value: any) => { + if (value == null || typeof value !== 'object') { + return value; + } else { + return this.toApiFieldNames(value); // recurse + } + }; + + // handle array and return early, or return a reduced object + + if (Array.isArray(apiData)) { + return apiData.map(getValueOrRecurse); + } + + return Object.keys(apiData).reduce((accum, field) => { + const value = apiData[field]; + let newName = field; + newName = snakeCase(newName); + newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m + newName = newName.replace('_in_bytes', '_bytes'); + newName = newName.replace('_in_millis', '_ms'); + + return { + ...accum, + [newName]: getValueOrRecurse(value), + }; + }, {}); + }; + + // TODO: remove + public map = (mapFn: any) => { + return this.collectors.map(mapFn); + }; + + // TODO: remove + public some = (someFn: any) => { + return this.collectors.some(someFn); + }; + + private makeCollectorSetFromArray = (collectors: Collector[]) => { + return new CollectorSet({ + logger: this.logger, + maximumWaitTimeForAllCollectorsInS: this.maximumWaitTimeForAllCollectorsInS, + collectors, + }); + }; +} diff --git a/src/legacy/server/usage/classes/index.js b/src/plugins/usage_collection/server/collector/index.ts similarity index 97% rename from src/legacy/server/usage/classes/index.js rename to src/plugins/usage_collection/server/collector/index.ts index 0d3939e1dc681..962f61474c250 100644 --- a/src/legacy/server/usage/classes/index.js +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -18,5 +18,7 @@ */ export { CollectorSet } from './collector_set'; +// @ts-ignore export { Collector } from './collector'; +// @ts-ignore export { UsageCollector } from './usage_collector'; diff --git a/src/legacy/server/usage/classes/usage_collector.js b/src/plugins/usage_collection/server/collector/usage_collector.js similarity index 88% rename from src/legacy/server/usage/classes/usage_collector.js rename to src/plugins/usage_collection/server/collector/usage_collector.js index 559deaef2ce15..1e2806ea15f3b 100644 --- a/src/legacy/server/usage/classes/usage_collector.js +++ b/src/plugins/usage_collection/server/collector/usage_collector.js @@ -17,20 +17,20 @@ * under the License. */ -import { KIBANA_STATS_TYPE } from '../../status/constants'; +import { KIBANA_STATS_TYPE } from '../../common/constants'; import { Collector } from './collector'; export class UsageCollector extends Collector { /* - * @param {Object} server - server object + * @param {Object} logger - logger object * @param {String} options.type - property name as the key for the data * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data * @param {Function} options.formatForBulkUpload - optional * @param {Function} options.rest - optional other properties */ - constructor(server, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { - super(server, { type, init, fetch, formatForBulkUpload, ...options }); + constructor(logger, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) { + super(logger, { type, init, fetch, formatForBulkUpload, ...options }); /* * Currently, for internal bulk uploading, usage stats are part of diff --git a/src/legacy/server/usage/lib/get_collector_logger.js b/src/plugins/usage_collection/server/config.ts similarity index 67% rename from src/legacy/server/usage/lib/get_collector_logger.js rename to src/plugins/usage_collection/server/config.ts index 023bf6bf635a8..987db1f2b0ff3 100644 --- a/src/legacy/server/usage/lib/get_collector_logger.js +++ b/src/plugins/usage_collection/server/config.ts @@ -17,15 +17,8 @@ * under the License. */ -const LOGGING_TAGS = ['stats-collection']; -/* - * @param {Object} server - * @return {Object} helpful logger object - */ -export function getCollectorLogger(server) { - return { - debug: message => server.log(['debug', ...LOGGING_TAGS], message), - info: message => server.log(['info', ...LOGGING_TAGS], message), - warn: message => server.log(['warning', ...LOGGING_TAGS], message) - }; -} +import { schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + maximumWaitTimeForAllCollectorsInS: schema.number({ defaultValue: 60 }), +}); diff --git a/src/legacy/server/usage/index.js b/src/plugins/usage_collection/server/index.ts similarity index 63% rename from src/legacy/server/usage/index.js rename to src/plugins/usage_collection/server/index.ts index 2a02070a55f95..33a1a0adc6713 100644 --- a/src/legacy/server/usage/index.js +++ b/src/plugins/usage_collection/server/index.ts @@ -17,15 +17,11 @@ * under the License. */ -import { CollectorSet } from './classes'; +import { PluginInitializerContext } from '../../../../src/core/server'; +import { Plugin } from './plugin'; +import { ConfigSchema } from './config'; -export function usageMixin(kbnServer, server, config) { - const collectorSet = new CollectorSet(server, undefined, config); - - /* - * expose the collector set object on the server - * provides factory methods for feature owners to create their own collector objects - * use collectorSet.register(collector) to register your feature's collector object(s) - */ - server.decorate('server', 'usage', { collectorSet }); -} +export { UsageCollectionSetup } from './plugin'; +export const config = { schema: ConfigSchema }; +export const plugin = (initializerContext: PluginInitializerContext) => + new Plugin(initializerContext); diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts new file mode 100644 index 0000000000000..e8bbc8e512a41 --- /dev/null +++ b/src/plugins/usage_collection/server/plugin.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { first } from 'rxjs/operators'; +import { TypeOf } from '@kbn/config-schema'; +import { ConfigSchema } from './config'; +import { PluginInitializerContext, Logger } from '../../../../src/core/server'; +import { CollectorSet } from './collector'; + +export type UsageCollectionSetup = CollectorSet; + +export class Plugin { + logger: Logger; + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public async setup(): Promise { + const config = await this.initializerContext.config + .create>() + .pipe(first()) + .toPromise(); + + const collectorSet = new CollectorSet({ + logger: this.logger, + maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, + }); + + return collectorSet; + } + + public start() { + this.logger.debug('Starting plugin'); + } + + public stop() { + this.logger.debug('Stopping plugin'); + } +} diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index 0cac20ef340d2..1784ed22a2b4d 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -108,7 +108,8 @@ export const apm: LegacyPluginInitializer = kibana => { } } }); - makeApmUsageCollector(server); + const { usageCollection } = server.newPlatform.setup.plugins; + makeApmUsageCollector(usageCollection, server); const apmPlugin = server.newPlatform.setup.plugins .apm as APMPluginContract; diff --git a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts index de8846a8f9fb4..ddfb4144d9636 100644 --- a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts @@ -13,6 +13,7 @@ import { APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID } from '../../../common/apm_saved_object_constants'; import { APMLegacyServer } from '../../routes/typings'; +import { UsageCollectionSetup } from '../../../../../../../src/plugins/usage_collection/server'; export function createApmTelementry( agentNames: string[] = [] @@ -43,8 +44,11 @@ export async function storeApmServicesTelemetry( } } -export function makeApmUsageCollector(server: APMLegacyServer) { - const apmUsageCollector = server.usage.collectorSet.makeUsageCollector({ +export function makeApmUsageCollector( + usageCollector: UsageCollectionSetup, + server: APMLegacyServer +) { + const apmUsageCollector = usageCollector.makeUsageCollector({ type: 'apm', fetch: async () => { const internalSavedObjectsClient = getInternalSavedObjectsClient(server); @@ -60,5 +64,6 @@ export function makeApmUsageCollector(server: APMLegacyServer) { }, isReady: () => true }); - server.usage.collectorSet.register(apmUsageCollector); + + usageCollector.registerCollector(apmUsageCollector); } diff --git a/x-pack/legacy/plugins/apm/server/routes/typings.ts b/x-pack/legacy/plugins/apm/server/routes/typings.ts index 207fe7fe5da33..9b114eba72626 100644 --- a/x-pack/legacy/plugins/apm/server/routes/typings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/typings.ts @@ -49,13 +49,7 @@ export interface Route< }) => Promise; } -export type APMLegacyServer = Pick & { - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; +export type APMLegacyServer = Pick & { plugins: { elasticsearch: Server['plugins']['elasticsearch']; }; diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js b/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js index ed83dbfcb75b7..141beb3d34d78 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/kibana.js @@ -29,12 +29,6 @@ export class Plugin { has: key => has(config, key), }), route: def => this.routes.push(def), - usage: { - collectorSet: { - makeUsageCollector: () => {}, - register: () => {}, - }, - }, }; const { init } = this.props; diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts index 888d9a5f36c32..b338971103381 100644 --- a/x-pack/legacy/plugins/canvas/server/plugin.ts +++ b/x-pack/legacy/plugins/canvas/server/plugin.ts @@ -61,7 +61,7 @@ export class Plugin { }, }); - registerCanvasUsageCollector(core, plugins); + registerCanvasUsageCollector(plugins.usageCollection, core); loadSampleData( plugins.sampleData.addSavedObjectsToSampleDataset, plugins.sampleData.addAppLinksToSampleDataset diff --git a/x-pack/legacy/plugins/canvas/server/shim.ts b/x-pack/legacy/plugins/canvas/server/shim.ts index c043f268af8ea..7641e51f14e56 100644 --- a/x-pack/legacy/plugins/canvas/server/shim.ts +++ b/x-pack/legacy/plugins/canvas/server/shim.ts @@ -8,6 +8,7 @@ import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { Legacy } from 'kibana'; import { CoreSetup as ExistingCoreSetup } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginSetupContract } from '../../../../plugins/features/server'; export interface CoreSetup { @@ -32,7 +33,7 @@ export interface PluginsSetup { addSavedObjectsToSampleDataset: any; addAppLinksToSampleDataset: any; }; - usage: Legacy.Server['usage']; + usageCollection: UsageCollectionSetup; } export async function createSetupShim( @@ -68,7 +69,7 @@ export async function createSetupShim( // @ts-ignore: Missing from Legacy Server Type addAppLinksToSampleDataset: server.addAppLinksToSampleDataset, }, - usage: server.usage, + usageCollection: server.newPlatform.setup.plugins.usageCollection, }, }; } diff --git a/x-pack/legacy/plugins/canvas/server/usage/collector.ts b/x-pack/legacy/plugins/canvas/server/usage/collector.ts index 7e6ef31d93ba5..ae009f9265722 100644 --- a/x-pack/legacy/plugins/canvas/server/usage/collector.ts +++ b/x-pack/legacy/plugins/canvas/server/usage/collector.ts @@ -5,7 +5,8 @@ */ import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { CoreSetup, PluginsSetup } from '../shim'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CoreSetup } from '../shim'; // @ts-ignore missing local declaration import { CANVAS_USAGE_TYPE } from '../../common/lib/constants'; import { workpadCollector } from './workpad_collector'; @@ -22,9 +23,12 @@ const collectors: TelemetryCollector[] = [workpadCollector, customElementCollect A usage collector function returns an object derived from current data in the ES Cluster. */ -export function registerCanvasUsageCollector(setup: CoreSetup, plugins: PluginsSetup) { - const kibanaIndex = setup.getServerConfig().get('kibana.index'); - const canvasCollector = plugins.usage.collectorSet.makeUsageCollector({ +export function registerCanvasUsageCollector( + usageCollection: UsageCollectionSetup, + core: CoreSetup +) { + const kibanaIndex = core.getServerConfig().get('kibana.index'); + const canvasCollector = usageCollection.makeUsageCollector({ type: CANVAS_USAGE_TYPE, isReady: () => true, fetch: async (callCluster: CallCluster) => { @@ -42,5 +46,5 @@ export function registerCanvasUsageCollector(setup: CoreSetup, plugins: PluginsS }, }); - plugins.usage.collectorSet.register(canvasCollector); + usageCollection.registerCollector(canvasCollector); } diff --git a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts b/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts similarity index 56% rename from x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts rename to x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts index ee80875890480..660cd256cebcd 100644 --- a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.test.ts +++ b/x-pack/legacy/plugins/cloud/cloud_usage_collector.test.ts @@ -5,37 +5,39 @@ */ import sinon from 'sinon'; -import { - createCollectorFetch, - getCloudUsageCollector, - KibanaHapiServer, -} from './get_cloud_usage_collector'; +import { Server } from 'hapi'; +import { createCollectorFetch, createCloudUsageCollector } from './cloud_usage_collector'; const CLOUD_ID_STAGING = 'staging:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; const CLOUD_ID = 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw=='; -const getMockServer = (cloudId?: string) => ({ - usage: { collectorSet: { makeUsageCollector: sinon.stub() } }, - config() { - return { - get(path: string) { - switch (path) { - case 'xpack.cloud': - return { id: cloudId }; - default: - throw Error(`server.config().get(${path}) should not be called by this collector.`); - } - }, - }; - }, +const mockUsageCollection = () => ({ + makeUsageCollector: sinon.stub(), }); +const getMockServer = (cloudId?: string) => + ({ + config() { + return { + get(path: string) { + switch (path) { + case 'xpack.cloud': + return { id: cloudId }; + default: + throw Error(`server.config().get(${path}) should not be called by this collector.`); + } + }, + }; + }, + } as Server); + describe('Cloud usage collector', () => { describe('collector', () => { it('returns `isCloudEnabled: false` if `xpack.cloud.id` is not defined', async () => { - const collector = await createCollectorFetch(getMockServer())(); + const mockServer = getMockServer(); + const collector = await createCollectorFetch(mockServer)(); expect(collector.isCloudEnabled).toBe(false); }); @@ -48,11 +50,11 @@ describe('Cloud usage collector', () => { }); }); -describe('getCloudUsageCollector', () => { - it('returns calls `collectorSet.makeUsageCollector`', () => { +describe('createCloudUsageCollector', () => { + it('returns calls `makeUsageCollector`', () => { const mockServer = getMockServer(); - getCloudUsageCollector((mockServer as any) as KibanaHapiServer); - const { makeUsageCollector } = mockServer.usage.collectorSet; - expect(makeUsageCollector.calledOnce).toBe(true); + const usageCollection = mockUsageCollection(); + createCloudUsageCollector(usageCollection as any, mockServer); + expect(usageCollection.makeUsageCollector.calledOnce).toBe(true); }); }); diff --git a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts b/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts similarity index 57% rename from x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts rename to x-pack/legacy/plugins/cloud/cloud_usage_collector.ts index 5ce7be59a1c9c..7fdf32144972c 100644 --- a/x-pack/legacy/plugins/cloud/get_cloud_usage_collector.ts +++ b/x-pack/legacy/plugins/cloud/cloud_usage_collector.ts @@ -5,21 +5,14 @@ */ import { Server } from 'hapi'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { KIBANA_CLOUD_STATS_TYPE } from './constants'; export interface UsageStats { isCloudEnabled: boolean; } -export interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: any; - }; - }; -} - -export function createCollectorFetch(server: any) { +export function createCollectorFetch(server: Server) { return async function fetchUsageStats(): Promise { const { id } = server.config().get(`xpack.cloud`); @@ -29,15 +22,15 @@ export function createCollectorFetch(server: any) { }; } -/* - * @param {Object} server - * @return {Object} kibana usage stats type collection object - */ -export function getCloudUsageCollector(server: KibanaHapiServer) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { + return usageCollection.makeUsageCollector({ type: KIBANA_CLOUD_STATS_TYPE, isReady: () => true, fetch: createCollectorFetch(server), }); } + +export function registerCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { + const collector = createCloudUsageCollector(usageCollection, server); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/legacy/plugins/cloud/index.js b/x-pack/legacy/plugins/cloud/index.js index 0cca122b52316..c2fd35eea5292 100644 --- a/x-pack/legacy/plugins/cloud/index.js +++ b/x-pack/legacy/plugins/cloud/index.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCloudUsageCollector } from './get_cloud_usage_collector'; +import { registerCloudUsageCollector } from './cloud_usage_collector'; export const cloud = kibana => { return new kibana.Plugin({ @@ -40,7 +40,8 @@ export const cloud = kibana => { server.expose('config', { isCloudEnabled: !!config.id }); - server.usage.collectorSet.register(getCloudUsageCollector(server)); + const { usageCollection } = server.newPlatform.setup.plugins; + registerCloudUsageCollector(usageCollection, server); } }); }; diff --git a/x-pack/legacy/plugins/file_upload/index.js b/x-pack/legacy/plugins/file_upload/index.js index 37d4ad80fa2ca..1eefc0afa8f9c 100644 --- a/x-pack/legacy/plugins/file_upload/index.js +++ b/x-pack/legacy/plugins/file_upload/index.js @@ -22,7 +22,10 @@ export const fileUpload = kibana => { init(server) { const coreSetup = server.newPlatform.setup.core; - const pluginsSetup = {}; + const { usageCollection } = server.newPlatform.setup.plugins; + const pluginsSetup = { + usageCollection, + }; // legacy dependencies const __LEGACY = { @@ -33,11 +36,6 @@ export const fileUpload = kibana => { savedObjects: { getSavedObjectsRepository: server.savedObjects.getSavedObjectsRepository }, - usage: { - collectorSet: { - makeUsageCollector: server.usage.collectorSet.makeUsageCollector - } - } }; new FileUploadPlugin().setup(coreSetup, pluginsSetup, __LEGACY); diff --git a/x-pack/legacy/plugins/file_upload/server/plugin.js b/x-pack/legacy/plugins/file_upload/server/plugin.js index 0baef6f8ffa40..d9819bf26faea 100644 --- a/x-pack/legacy/plugins/file_upload/server/plugin.js +++ b/x-pack/legacy/plugins/file_upload/server/plugin.js @@ -5,16 +5,13 @@ */ import { getImportRouteHandler } from './routes/file_upload'; -import { getTelemetry, initTelemetry } from './telemetry/telemetry'; import { MAX_BYTES } from '../common/constants/file_import'; - -const TELEMETRY_TYPE = 'fileUploadTelemetry'; +import { registerFileUploadUsageCollector } from './telemetry'; export class FileUploadPlugin { setup(core, plugins, __LEGACY) { const elasticsearchPlugin = __LEGACY.plugins.elasticsearch; const getSavedObjectsRepository = __LEGACY.savedObjects.getSavedObjectsRepository; - const makeUsageCollector = __LEGACY.usage.collectorSet.makeUsageCollector; // Set up route __LEGACY.route({ @@ -26,11 +23,9 @@ export class FileUploadPlugin { } }); - // Make usage collector - makeUsageCollector({ - type: TELEMETRY_TYPE, - isReady: () => true, - fetch: async () => (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry() + registerFileUploadUsageCollector(plugins.usageCollection, { + elasticsearchPlugin, + getSavedObjectsRepository, }); } } diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts new file mode 100644 index 0000000000000..a2b359ae11638 --- /dev/null +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { getTelemetry, initTelemetry } from './telemetry'; + +const TELEMETRY_TYPE = 'fileUploadTelemetry'; + +export function registerFileUploadUsageCollector( + usageCollection: UsageCollectionSetup, + deps: { + elasticsearchPlugin: any; + getSavedObjectsRepository: any; + } +): void { + const { elasticsearchPlugin, getSavedObjectsRepository } = deps; + const fileUploadUsageCollector = usageCollection.makeUsageCollector({ + type: TELEMETRY_TYPE, + isReady: () => true, + fetch: async () => + (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry(), + }); + + usageCollection.registerCollector(fileUploadUsageCollector); +} diff --git a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts index 46da040dc34f0..7969dd04ce31f 100644 --- a/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts +++ b/x-pack/legacy/plugins/file_upload/server/telemetry/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './telemetry'; +export { registerFileUploadUsageCollector } from './file_upload_usage_collector'; diff --git a/x-pack/legacy/plugins/infra/server/kibana.index.ts b/x-pack/legacy/plugins/infra/server/kibana.index.ts index 48ef846ec5275..91bcd6be95a75 100644 --- a/x-pack/legacy/plugins/infra/server/kibana.index.ts +++ b/x-pack/legacy/plugins/infra/server/kibana.index.ts @@ -13,11 +13,8 @@ import { UsageCollector } from './usage/usage_collector'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; -export interface KbnServer extends Server { - usage: any; -} - -export const initServerWithKibana = (kbnServer: KbnServer) => { +export const initServerWithKibana = (kbnServer: Server) => { + const { usageCollection } = kbnServer.newPlatform.setup.plugins; const libs = compose(kbnServer); initInfraServer(libs); @@ -27,7 +24,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => { ); // Register a function with server to manage the collection of usage stats - kbnServer.usage.collectorSet.register(UsageCollector.getUsageCollector(kbnServer)); + UsageCollector.registerUsageCollector(usageCollection); const xpackMainPlugin = kbnServer.plugins.xpack_main; xpackMainPlugin.registerFeature({ diff --git a/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts b/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts index 018c903009bbe..601beddc0a2db 100644 --- a/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts +++ b/x-pack/legacy/plugins/infra/server/usage/usage_collector.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { InfraNodeType } from '../graphql/types'; -import { KbnServer } from '../kibana.index'; - const KIBANA_REPORTING_TYPE = 'infraops'; interface InfraopsSum { @@ -17,10 +16,13 @@ interface InfraopsSum { } export class UsageCollector { - public static getUsageCollector(server: KbnServer) { - const { collectorSet } = server.usage; + public static registerUsageCollector(usageCollection: UsageCollectionSetup): void { + const collector = UsageCollector.getUsageCollector(usageCollection); + usageCollection.registerCollector(collector); + } - return collectorSet.makeUsageCollector({ + public static getUsageCollector(usageCollection: UsageCollectionSetup) { + return usageCollection.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, isReady: () => true, fetch: async () => { diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index d4cea28d14085..a79b9907f6437 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -58,10 +58,12 @@ export const lens: LegacyPluginInitializer = kibana => { // Set up with the new platform plugin lifecycle API. const plugin = lensServerPlugin(); + const { usageCollection } = server.newPlatform.setup.plugins; + plugin.setup(kbnServer.newPlatform.setup.core, { + usageCollection, // Legacy APIs savedObjects: server.savedObjects, - usage: server.usage, config: server.config(), server, }); diff --git a/x-pack/legacy/plugins/lens/server/plugin.tsx b/x-pack/legacy/plugins/lens/server/plugin.tsx index a4c8e9b268df5..0223b90c37046 100644 --- a/x-pack/legacy/plugins/lens/server/plugin.tsx +++ b/x-pack/legacy/plugins/lens/server/plugin.tsx @@ -6,27 +6,22 @@ import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; import { Plugin, CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { setupRoutes } from './routes'; import { registerLensUsageCollector, initializeLensTelemetry } from './usage'; +export interface PluginSetupContract { + savedObjects: SavedObjectsLegacyService; + usageCollection: UsageCollectionSetup; + config: KibanaConfig; + server: Server; +} + export class LensServer implements Plugin<{}, {}, {}, {}> { - setup( - core: CoreSetup, - plugins: { - savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; - config: KibanaConfig; - server: Server; - } - ) { + setup(core: CoreSetup, plugins: PluginSetupContract) { setupRoutes(core, plugins); - registerLensUsageCollector(core, plugins); - initializeLensTelemetry(core, plugins); + registerLensUsageCollector(plugins.usageCollection, plugins.server); + initializeLensTelemetry(core, plugins.server); return {}; } diff --git a/x-pack/legacy/plugins/lens/server/usage/collectors.ts b/x-pack/legacy/plugins/lens/server/usage/collectors.ts index 94a7c8e0d85c1..274b72c33e59a 100644 --- a/x-pack/legacy/plugins/lens/server/usage/collectors.ts +++ b/x-pack/legacy/plugins/lens/server/usage/collectors.ts @@ -6,29 +6,17 @@ import moment from 'moment'; import { get } from 'lodash'; -import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; -import { CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; +import { Server } from 'src/legacy/server/kbn_server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + import { LensUsage, LensTelemetryState } from './types'; -export function registerLensUsageCollector( - core: CoreSetup, - plugins: { - savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; - config: KibanaConfig; - server: Server; - } -) { +export function registerLensUsageCollector(usageCollection: UsageCollectionSetup, server: Server) { let isCollectorReady = false; async function determineIfTaskManagerIsReady() { let isReady = false; try { - isReady = await isTaskManagerReady(plugins.server); + isReady = await isTaskManagerReady(server); } catch (err) {} // eslint-disable-line if (isReady) { @@ -39,11 +27,11 @@ export function registerLensUsageCollector( } determineIfTaskManagerIsReady(); - const lensUsageCollector = plugins.usage.collectorSet.makeUsageCollector({ + const lensUsageCollector = usageCollection.makeUsageCollector({ type: 'lens', fetch: async (): Promise => { try { - const docs = await getLatestTaskState(plugins.server); + const docs = await getLatestTaskState(server); // get the accumulated state from the recurring task const state: LensTelemetryState = get(docs, '[0].state'); @@ -75,7 +63,8 @@ export function registerLensUsageCollector( }, isReady: () => isCollectorReady, }); - plugins.usage.collectorSet.register(lensUsageCollector); + + usageCollection.registerCollector(lensUsageCollector); } function addEvents(prevEvents: Record, newEvents: Record) { diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts index 03e085cc9e669..feb73538f44f0 100644 --- a/x-pack/legacy/plugins/lens/server/usage/task.ts +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -39,12 +39,12 @@ type ClusterDeleteType = ( options?: CallClusterOptions ) => Promise; -export function initializeLensTelemetry(core: CoreSetup, { server }: { server: Server }) { - registerLensTelemetryTask(core, { server }); +export function initializeLensTelemetry(core: CoreSetup, server: Server) { + registerLensTelemetryTask(core, server); scheduleTasks(server); } -function registerLensTelemetryTask(core: CoreSetup, { server }: { server: Server }) { +function registerLensTelemetryTask(core: CoreSetup, server: Server) { const taskManager = server.plugins.task_manager; if (!taskManager) { diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 739e98beec10f..c59fbe42a1754 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -101,12 +101,12 @@ export function maps(kibana) { init(server) { const mapsEnabled = server.config().get('xpack.maps.enabled'); - + const { usageCollection } = server.newPlatform.setup.plugins; if (!mapsEnabled) { server.log(['info', 'maps'], 'Maps app disabled by configuration'); return; } - initTelemetryCollection(server); + initTelemetryCollection(usageCollection, server); const xpackMainPlugin = server.plugins.xpack_main; let routesInitialized = false; diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js index c0ac5a781b796..c4d755b5908f0 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_usage_collector.js @@ -7,10 +7,10 @@ import _ from 'lodash'; import { TASK_ID, scheduleTask, registerMapsTelemetryTask } from './telemetry_task'; -export function initTelemetryCollection(server) { +export function initTelemetryCollection(usageCollection, server) { registerMapsTelemetryTask(server); scheduleTask(server); - registerMapsUsageCollector(server); + registerMapsUsageCollector(usageCollection, server); } async function isTaskManagerReady(server) { @@ -81,9 +81,8 @@ export function buildCollectorObj(server) { }; } -export function registerMapsUsageCollector(server) { +export function registerMapsUsageCollector(usageCollection, server) { const collectorObj = buildCollectorObj(server); - const mapsUsageCollector = server.usage.collectorSet - .makeUsageCollector(collectorObj); - server.usage.collectorSet.register(mapsUsageCollector); + const mapsUsageCollector = usageCollection.makeUsageCollector(collectorObj); + usageCollection.registerCollector(mapsUsageCollector); } diff --git a/x-pack/legacy/plugins/maps/server/test_utils/index.js b/x-pack/legacy/plugins/maps/server/test_utils/index.js index 13b7c56d6fc8b..e9f97101759f0 100644 --- a/x-pack/legacy/plugins/maps/server/test_utils/index.js +++ b/x-pack/legacy/plugins/maps/server/test_utils/index.js @@ -40,12 +40,6 @@ export const getMockKbnServer = ( fetch: mockTaskFetch, }, }, - usage: { - collectorSet: { - makeUsageCollector: () => '', - register: () => undefined, - }, - }, config: () => ({ get: () => '' }), log: () => undefined }); diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 3cafa232f0744..90e1e748492cb 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -79,7 +79,6 @@ export const ml = (kibana: any) => { injectUiAppVars: server.injectUiAppVars, http: mlHttpService, savedObjects: server.savedObjects, - usage: server.usage, }; const plugins = { @@ -87,6 +86,7 @@ export const ml = (kibana: any) => { security: server.plugins.security, xpackMain: server.plugins.xpack_main, spaces: server.plugins.spaces, + usageCollection: kbnServer.newPlatform.setup.plugins.usageCollection, ml: this, }; diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts index 6bc98ba68f60b..7a9766f36a6ed 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { createMlTelemetry, getSavedObjectsClient, @@ -14,12 +15,11 @@ import { import { UsageInitialization } from '../../new_platform/plugin'; -export function makeMlUsageCollector({ - elasticsearchPlugin, - usage, - savedObjects, -}: UsageInitialization): void { - const mlUsageCollector = usage.collectorSet.makeUsageCollector({ +export function makeMlUsageCollector( + usageCollection: UsageCollectionSetup, + { elasticsearchPlugin, savedObjects }: UsageInitialization +): void { + const mlUsageCollector = usageCollection.makeUsageCollector({ type: 'ml', isReady: () => true, fetch: async (): Promise => { @@ -35,5 +35,6 @@ export function makeMlUsageCollector({ } }, }); - usage.collectorSet.register(mlUsageCollector); + + usageCollection.registerCollector(mlUsageCollector); } diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index b2b697a851703..b789121beebfc 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -10,6 +10,7 @@ import { ServerRoute } from 'hapi'; import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; import { checkLicense } from '../lib/check_license'; @@ -68,12 +69,6 @@ export interface MlCoreSetup { injectUiAppVars: (id: string, callback: () => {}) => any; http: MlHttpServiceSetup; savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - makeUsageCollector: any; - register: (collector: any) => void; - }; - }; } export interface MlInitializerContext extends PluginInitializerContext { legacyConfig: KibanaConfig; @@ -84,6 +79,7 @@ export interface PluginsSetup { xpackMain: MlXpackMainPlugin; security: any; spaces: any; + usageCollection: UsageCollectionSetup; // TODO: this is temporary for `mirrorPluginStatus` ml: any; } @@ -98,12 +94,6 @@ export interface RouteInitialization { } export interface UsageInitialization { elasticsearchPlugin: ElasticsearchPlugin; - usage: { - collectorSet: { - makeUsageCollector: any; - register: (collector: any) => void; - }; - }; savedObjects: SavedObjectsLegacyService; } @@ -201,10 +191,8 @@ export class Plugin { savedObjects: core.savedObjects, spacesPlugin: plugins.spaces, }; - const usageInitializationDeps: UsageInitialization = { elasticsearchPlugin: plugins.elasticsearch, - usage: core.usage, savedObjects: core.savedObjects, }; @@ -231,7 +219,7 @@ export class Plugin { fileDataVisualizerRoutes(extendedRouteInitializationDeps); initMlServerLog(logInitializationDeps); - makeMlUsageCollector(usageInitializationDeps); + makeMlUsageCollector(plugins.usageCollection, usageInitializationDeps); } public stop() {} diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index 97046bfb7d5b4..79db8cb920ea3 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -56,9 +56,6 @@ export const monitoring = (kibana) => new kibana.Plugin({ throw `Unknown key '${key}'`; } }), - usage: { - collectorSet: server.usage.collectorSet - }, injectUiAppVars: server.injectUiAppVars, log: (...args) => server.log(...args), getOSInfo: server.getOSInfo, @@ -70,11 +67,12 @@ export const monitoring = (kibana) => new kibana.Plugin({ _hapi: server, _kbnServer: this.kbnServer }; - + const { usageCollection } = server.newPlatform.setup.plugins; const plugins = { xpack_main: server.plugins.xpack_main, elasticsearch: server.plugins.elasticsearch, infra: server.plugins.infra, + usageCollection, }; new Plugin().setup(serverFacade, plugins); diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index da23d4b77a323..b0367bc078473 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -68,21 +68,21 @@ export class BulkUploader { /* * Start the interval timer - * @param {CollectorSet} collectorSet object to use for initial the fetch/upload and fetch/uploading on interval + * @param {usageCollection} usageCollection object to use for initial the fetch/upload and fetch/uploading on interval * @return undefined */ - start(collectorSet) { + start(usageCollection) { this._log.info('Starting monitoring stats collection'); - const filterCollectorSet = _collectorSet => { + const filterCollectorSet = _usageCollection => { const successfulUploadInLastDay = this._lastFetchUsageTime && this._lastFetchUsageTime + this._usageInterval > Date.now(); - return _collectorSet.getFilteredCollectorSet(c => { + return _usageCollection.getFilteredCollectorSet(c => { // this is internal bulk upload, so filter out API-only collectors if (c.ignoreForInternalUploader) { return false; } // Only collect usage data at the same interval as telemetry would (default to once a day) - if (successfulUploadInLastDay && _collectorSet.isUsageCollector(c)) { + if (successfulUploadInLastDay && _usageCollection.isUsageCollector(c)) { return false; } return true; @@ -92,11 +92,11 @@ export class BulkUploader { if (this._timer) { clearInterval(this._timer); } else { - this._fetchAndUpload(filterCollectorSet(collectorSet)); // initial fetch + this._fetchAndUpload(filterCollectorSet(usageCollection)); // initial fetch } this._timer = setInterval(() => { - this._fetchAndUpload(filterCollectorSet(collectorSet)); + this._fetchAndUpload(filterCollectorSet(usageCollection)); }, this._interval); } @@ -121,12 +121,12 @@ export class BulkUploader { } /* - * @param {CollectorSet} collectorSet + * @param {usageCollection} usageCollection * @return {Promise} - resolves to undefined */ - async _fetchAndUpload(collectorSet) { - const collectorsReady = await collectorSet.areAllCollectorsReady(); - const hasUsageCollectors = collectorSet.some(collectorSet.isUsageCollector); + async _fetchAndUpload(usageCollection) { + const collectorsReady = await usageCollection.areAllCollectorsReady(); + const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector); if (!collectorsReady) { this._log.debug('Skipping bulk uploading because not all collectors are ready'); if (hasUsageCollectors) { @@ -136,8 +136,8 @@ export class BulkUploader { return; } - const data = await collectorSet.bulkFetch(this._callClusterWithInternalUser); - const payload = this.toBulkUploadFormat(compact(data), collectorSet); + const data = await usageCollection.bulkFetch(this._callClusterWithInternalUser); + const payload = this.toBulkUploadFormat(compact(data), usageCollection); if (payload) { try { @@ -202,7 +202,7 @@ export class BulkUploader { * } * ] */ - toBulkUploadFormat(rawData, collectorSet) { + toBulkUploadFormat(rawData, usageCollection) { if (rawData.length === 0) { return; } @@ -210,7 +210,7 @@ export class BulkUploader { // convert the raw data to a nested object by taking each payload through // its formatter, organizing it per-type const typesNested = rawData.reduce((accum, { type, result }) => { - const { type: uploadType, payload: uploadData } = collectorSet.getCollectorByType(type).formatForBulkUpload(result); + const { type: uploadType, payload: uploadData } = usageCollection.getCollectorByType(type).formatForBulkUpload(result); return defaultsDeep(accum, { [uploadType]: uploadData }); }, {}); // convert the nested object into a flat array, with each payload prefixed diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js index 25efc63fafb5d..5d2ebf8dc2abc 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_kibana_usage_collector.js @@ -19,8 +19,8 @@ const TYPES = [ /** * Fetches saved object counts by querying the .kibana index */ -export function getKibanaUsageCollector({ collectorSet, config }) { - return collectorSet.makeUsageCollector({ +export function getKibanaUsageCollector(usageCollection, config) { + return usageCollection.makeUsageCollector({ type: KIBANA_USAGE_TYPE, isReady: () => true, async fetch(callCluster) { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js index f1f47761d9f0c..2c0250fb78592 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_ops_stats_collector.js @@ -49,14 +49,13 @@ class OpsMonitor { /* * Initialize a collector for Kibana Ops Stats */ -export function getOpsStatsCollector({ +export function getOpsStatsCollector(usageCollection, { elasticsearchPlugin, kbnServerConfig, log, config, getOSInfo, hapiServer, - collectorSet }) { const buffer = opsBuffer({ log, config, getOSInfo }); const interval = kbnServerConfig.get('ops.interval'); @@ -85,7 +84,7 @@ export function getOpsStatsCollector({ }, 5 * 1000); // wait 5 seconds to avoid race condition with reloading logging configuration }); - return collectorSet.makeStatsCollector({ + return usageCollection.makeStatsCollector({ type: KIBANA_STATS_TYPE_MONITORING, init: opsMonitor.start, isReady: () => { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js index bb561ddda42ab..2a56deaad4f8a 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js @@ -46,8 +46,8 @@ export async function checkForEmailValue( } } -export function getSettingsCollector({ config, collectorSet }) { - return collectorSet.makeStatsCollector({ +export function getSettingsCollector(usageCollection, config) { + return usageCollection.makeStatsCollector({ type: KIBANA_SETTINGS_TYPE, isReady: () => true, async fetch(callCluster) { diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js index 3c8eb5ebdf2d3..1099a23dea103 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/collectors/index.js @@ -4,6 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getKibanaUsageCollector } from './get_kibana_usage_collector'; -export { getOpsStatsCollector } from './get_ops_stats_collector'; -export { getSettingsCollector } from './get_settings_collector'; +import { getKibanaUsageCollector } from './get_kibana_usage_collector'; +import { getOpsStatsCollector } from './get_ops_stats_collector'; +import { getSettingsCollector } from './get_settings_collector'; + +export function registerCollectors(usageCollection, collectorsConfigs) { + const { config } = collectorsConfigs; + + usageCollection.registerCollector(getOpsStatsCollector(usageCollection, collectorsConfigs)); + usageCollection.registerCollector(getKibanaUsageCollector(usageCollection, config)); + usageCollection.registerCollector(getSettingsCollector(usageCollection, config)); +} diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js index ae691f49e2b80..c202fe9589ab3 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/index.js @@ -5,3 +5,4 @@ */ export { initBulkUploader } from './init'; +export { registerCollectors } from './collectors'; diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js index bb42dad26786a..36f085c424881 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js @@ -13,6 +13,17 @@ const liveClusterUuid = 'a12'; const mockReq = (searchResult = {}) => { return { server: { + newPlatform: { + setup: { + plugins: { + usageCollection: { + getCollectorByType: () => ({ + isReady: () => false + }), + }, + }, + }, + }, config() { return { get: sinon.stub() diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index d25d8af4aaa20..540de7d1e3a7f 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -273,13 +273,15 @@ function shouldSkipBucket(product, bucket) { return false; } -async function getLiveKibanaInstance(req) { - const { collectorSet } = req.server.usage; - const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE); +async function getLiveKibanaInstance(usageCollection) { + if (!usageCollection) { + return null; + } + const kibanaStatsCollector = usageCollection.getCollectorByType(KIBANA_STATS_TYPE); if (!await kibanaStatsCollector.isReady()) { return null; } - return collectorSet.toApiFieldNames(await kibanaStatsCollector.fetch()); + return usageCollection.toApiFieldNames(await kibanaStatsCollector.fetch()); } async function getLiveElasticsearchClusterUuid(req) { @@ -341,9 +343,11 @@ async function getLiveElasticsearchCollectionEnabled(req) { * @param {*} skipLiveData Optional and will not make any live api calls if set to true */ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeUuid, skipLiveData) => { + const config = req.server.config(); const kibanaUuid = config.get('server.uuid'); const hasPermissions = await hasNecessaryPermissions(req); + if (!hasPermissions) { return { _meta: { @@ -351,6 +355,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU } }; } + console.log('OKOKOKOK'); const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; @@ -372,7 +377,8 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU const liveEsNodes = skipLiveData || !isLiveCluster ? [] : await getLivesNodes(req); - const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(req); + const { usageCollection } = req.server.newPlatform.setup.plugins; + const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(usageCollection); const indicesBuckets = get(recentDocuments, 'aggregations.indices.buckets', []); const liveClusterInternalCollectionEnabled = await getLiveElasticsearchCollectionEnabled(req); diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index 48a02109a3f6f..97930610e0593 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -9,35 +9,27 @@ import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../common/constants' import { requireUIRoutes } from './routes'; import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; -import { initBulkUploader } from './kibana_monitoring'; +import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; -import { - getKibanaUsageCollector, - getOpsStatsCollector, - getSettingsCollector, -} from './kibana_monitoring/collectors'; - export class Plugin { setup(core, plugins) { const kbnServer = core._kbnServer; const config = core.config(); - const { collectorSet } = core.usage; + const usageCollection = plugins.usageCollection; + registerMonitoringCollection(); /* * Register collector objects for stats to show up in the APIs */ - collectorSet.register(getOpsStatsCollector({ + registerCollectors(usageCollection, { elasticsearchPlugin: plugins.elasticsearch, kbnServerConfig: kbnServer.config, log: core.log, config, getOSInfo: core.getOSInfo, hapiServer: core._hapi, - collectorSet: core.usage.collectorSet, - })); - collectorSet.register(getKibanaUsageCollector({ collectorSet, config })); - collectorSet.register(getSettingsCollector({ collectorSet, config })); - registerMonitoringCollection(); + }); + /* * Instantiate and start the internal background task that calls collector @@ -110,7 +102,7 @@ export class Plugin { const mainMonitoring = xpackMainInfo.feature('monitoring'); const monitoringBulkEnabled = mainMonitoring && mainMonitoring.isAvailable() && mainMonitoring.isEnabled(); if (monitoringBulkEnabled) { - bulkUploader.start(collectorSet); + bulkUploader.start(usageCollection); } else { bulkUploader.handleNotEnabled(); } diff --git a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js index a0072e52fc7f7..c6bb368745830 100644 --- a/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js @@ -8,9 +8,8 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { getClusterUuids, fetchClusterUuids, handleClusterUuidsResponse } from '../get_cluster_uuids'; -// FAILING: https://github.com/elastic/kibana/issues/51371 -describe.skip('get_cluster_uuids', () => { - const callWith = sinon.stub(); +describe('get_cluster_uuids', () => { + const callCluster = sinon.stub(); const size = 123; const server = { config: sinon.stub().returns({ @@ -29,23 +28,23 @@ describe.skip('get_cluster_uuids', () => { } } }; - const expectedUuids = response.aggregations.cluster_uuids.buckets.map(bucket => bucket.key); + const expectedUuids = response.aggregations.cluster_uuids.buckets + .map(bucket => bucket.key) + .map(expectedUuid => ({ clusterUuid: expectedUuid })); const start = new Date(); const end = new Date(); describe('getClusterUuids', () => { it('returns cluster UUIDs', async () => { - callWith.withArgs('search').returns(Promise.resolve(response)); - - expect(await getClusterUuids(server, callWith, start, end)).to.eql(expectedUuids); + callCluster.withArgs('search').returns(Promise.resolve(response)); + expect(await getClusterUuids({ server, callCluster, start, end })).to.eql(expectedUuids); }); }); describe('fetchClusterUuids', () => { it('searches for clusters', async () => { - callWith.returns(Promise.resolve(response)); - - expect(await fetchClusterUuids(server, callWith, start, end)).to.be(response); + callCluster.returns(Promise.resolve(response)); + expect(await fetchClusterUuids({ server, callCluster, start, end })).to.be(response); }); }); @@ -53,13 +52,11 @@ describe.skip('get_cluster_uuids', () => { // filterPath makes it easy to ignore anything unexpected because it will come back empty it('handles unexpected response', () => { const clusterUuids = handleClusterUuidsResponse({}); - expect(clusterUuids.length).to.be(0); }); it('handles valid response', () => { const clusterUuids = handleClusterUuidsResponse(response); - expect(clusterUuids).to.eql(expectedUuids); }); diff --git a/x-pack/legacy/plugins/oss_telemetry/index.d.ts b/x-pack/legacy/plugins/oss_telemetry/index.d.ts index 012f987627369..1b592dabf2053 100644 --- a/x-pack/legacy/plugins/oss_telemetry/index.d.ts +++ b/x-pack/legacy/plugins/oss_telemetry/index.d.ts @@ -54,12 +54,6 @@ export interface HapiServer { }>; }; }; - usage: { - collectorSet: { - register: (collector: any) => void; - makeUsageCollector: (collectorOpts: any) => void; - }; - }; config: () => { get: (prop: string) => any; }; diff --git a/x-pack/legacy/plugins/oss_telemetry/index.js b/x-pack/legacy/plugins/oss_telemetry/index.js index eeee9e18f9112..f86baef020aa2 100644 --- a/x-pack/legacy/plugins/oss_telemetry/index.js +++ b/x-pack/legacy/plugins/oss_telemetry/index.js @@ -15,7 +15,8 @@ export const ossTelemetry = (kibana) => { configPrefix: 'xpack.oss_telemetry', init(server) { - registerCollectors(server); + const { usageCollection } = server.newPlatform.setup.plugins; + registerCollectors(usageCollection, server); registerTasks(server); scheduleTasks(server); } diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts index 8b825b13178f2..0121ed4304d26 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HapiServer } from '../../../'; import { registerVisualizationsCollector } from './visualizations/register_usage_collector'; -export function registerCollectors(server: HapiServer) { - registerVisualizationsCollector(server); +export function registerCollectors(usageCollection: UsageCollectionSetup, server: HapiServer) { + registerVisualizationsCollector(usageCollection, server); } diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts index 555c7ac27b49d..09843a6f87ad7 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HapiServer } from '../../../../'; import { getUsageCollector } from './get_usage_collector'; -export function registerVisualizationsCollector(server: HapiServer): void { - const { usage } = server; - const collector = usage.collectorSet.makeUsageCollector(getUsageCollector(server)); - usage.collectorSet.register(collector); +export function registerVisualizationsCollector( + usageCollection: UsageCollectionSetup, + server: HapiServer +): void { + const collector = usageCollection.makeUsageCollector(getUsageCollector(server)); + usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts index 998a1d2beeab1..1cebe78b9c7f0 100644 --- a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts @@ -54,12 +54,6 @@ export const getMockKbnServer = ( fetch: mockTaskFetch, }, }, - usage: { - collectorSet: { - makeUsageCollector: () => '', - register: () => undefined, - }, - }, config: () => mockConfig, log: () => undefined, }); diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index e2b5970d1efb7..9add3accd262f 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -20,7 +20,7 @@ import { import { config as reportingConfig } from './config'; import { logConfiguration } from './log_configuration'; import { createBrowserDriverFactory } from './server/browsers'; -import { getReportingUsageCollector } from './server/usage'; +import { registerReportingUsageCollector } from './server/usage'; import { ReportingConfigOptions, ReportingPluginSpecOptions, ServerFacade } from './types.d'; const kbToBase64Length = (kb: number) => { @@ -76,9 +76,8 @@ export const reporting = (kibana: any) => { async init(server: ServerFacade) { let isCollectorReady = false; // Register a function with server to manage the collection of usage stats - server.usage.collectorSet.register( - getReportingUsageCollector(server, () => isCollectorReady) - ); + const { usageCollection } = server.newPlatform.setup.plugins; + registerReportingUsageCollector(usageCollection, server, () => isCollectorReady); const logger = LevelLogger.createForServer(server, [PLUGIN_ID]); const [exportTypesRegistry, browserFactory] = await Promise.all([ diff --git a/x-pack/legacy/plugins/reporting/server/usage/index.ts b/x-pack/legacy/plugins/reporting/server/usage/index.ts index 91e2a9284550b..141ecb9c77656 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/index.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getReportingUsageCollector } from './get_reporting_usage_collector'; +export { registerReportingUsageCollector } from './reporting_usage_collector'; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js similarity index 93% rename from x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js rename to x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js index 32022c6fa642c..f23f679865146 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.test.js +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js @@ -4,15 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ import sinon from 'sinon'; -import { getReportingUsageCollector } from './get_reporting_usage_collector'; +import { getReportingUsageCollector } from './reporting_usage_collector'; -function getServerMock(customization) { +function getMockUsageCollection() { class MockUsageCollector { constructor(_server, { fetch }) { this.fetch = fetch; } } + return { + makeUsageCollector: options => { + return new MockUsageCollector(this, options); + }, + }; +} +function getServerMock(customization) { const getLicenseCheckResults = sinon.stub().returns({}); const defaultServerMock = { plugins: { @@ -44,13 +51,6 @@ function getServerMock(customization) { } }, }), - usage: { - collectorSet: { - makeUsageCollector: options => { - return new MockUsageCollector(this, options); - }, - }, - }, }; return Object.assign(defaultServerMock, customization); } @@ -66,7 +66,8 @@ describe('license checks', () => { .stub() .returns('basic'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); usageStats = await getReportingUsage(callClusterMock); }); @@ -91,7 +92,8 @@ describe('license checks', () => { .stub() .returns('none'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithNoLicenseMock); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithNoLicenseMock); usageStats = await getReportingUsage(callClusterMock); }); @@ -116,7 +118,9 @@ describe('license checks', () => { .stub() .returns('platinum'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); + const usageCollection = getMockUsageCollection(); const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, serverWithPlatinumLicenseMock ); usageStats = await getReportingUsage(callClusterMock); @@ -143,7 +147,8 @@ describe('license checks', () => { .stub() .returns('basic'); const callClusterMock = jest.fn(() => Promise.resolve({})); - const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock); + const usageCollection = getMockUsageCollection(); + const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); usageStats = await getReportingUsage(callClusterMock); }); @@ -160,11 +165,12 @@ describe('license checks', () => { describe('data modeling', () => { let getReportingUsage; beforeAll(async () => { + const usageCollection = getMockUsageCollection(); const serverWithPlatinumLicenseMock = getServerMock(); serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon .stub() .returns('platinum'); - ({ fetch: getReportingUsage } = getReportingUsageCollector(serverWithPlatinumLicenseMock)); + ({ fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithPlatinumLicenseMock)); }); test('with normal looking usage data', async () => { diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts similarity index 70% rename from x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts rename to x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts index 5c52193769057..0a7ef0a194434 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; // @ts-ignore untyped module import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; import { ServerFacade, ESCallCluster } from '../../types'; @@ -15,9 +16,12 @@ import { RangeStats } from './types'; * @param {Object} server * @return {Object} kibana usage stats type collection object */ -export function getReportingUsageCollector(server: ServerFacade, isReady: () => boolean) { - const { collectorSet } = server.usage; - return collectorSet.makeUsageCollector({ +export function getReportingUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerFacade, + isReady: () => boolean +) { + return usageCollection.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, isReady, fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster), @@ -41,3 +45,12 @@ export function getReportingUsageCollector(server: ServerFacade, isReady: () => }, }); } + +export function registerReportingUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerFacade, + isReady: () => boolean +) { + const collector = getReportingUsageCollector(usageCollection, server, isReady); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/legacy/plugins/rollup/index.js b/x-pack/legacy/plugins/rollup/index.js index 3b6c033a2d85a..e0c00a7db62f0 100644 --- a/x-pack/legacy/plugins/rollup/index.js +++ b/x-pack/legacy/plugins/rollup/index.js @@ -57,12 +57,13 @@ export function rollup(kibana) { ], }, init: function (server) { + const { usageCollection } = server.newPlatform.setup.plugins; registerLicenseChecker(server); registerIndicesRoute(server); registerFieldsForWildcardRoute(server); registerSearchRoute(server); registerJobsRoute(server); - registerRollupUsageCollector(server); + registerRollupUsageCollector(usageCollection, server); if ( server.plugins.index_management && server.plugins.index_management.addIndexManagementDataEnricher diff --git a/x-pack/legacy/plugins/rollup/server/usage/collector.js b/x-pack/legacy/plugins/rollup/server/usage/collector.js index 977253dfa53fb..99fffa774baaf 100644 --- a/x-pack/legacy/plugins/rollup/server/usage/collector.js +++ b/x-pack/legacy/plugins/rollup/server/usage/collector.js @@ -163,10 +163,10 @@ async function fetchRollupVisualizations(kibanaIndex, callCluster, rollupIndexPa }; } -export function registerRollupUsageCollector(server) { +export function registerRollupUsageCollector(usageCollection, server) { const kibanaIndex = server.config().get('kibana.index'); - const collector = server.usage.collectorSet.makeUsageCollector({ + const collector = usageCollection.makeUsageCollector({ type: ROLLUP_USAGE_TYPE, isReady: () => true, fetch: async callCluster => { @@ -198,5 +198,5 @@ export function registerRollupUsageCollector(server) { }, }); - server.usage.collectorSet.register(collector); + usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 598d115a39e49..8f995d3c12c2a 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -126,7 +126,6 @@ export const spaces = (kibana: Record) => kibanaIndex: config.get('kibana.index'), }, savedObjects: server.savedObjects, - usage: server.usage, tutorial: { addScopedTutorialContextFactory: server.addScopedTutorialContextFactory, }, diff --git a/x-pack/legacy/plugins/upgrade_assistant/index.ts b/x-pack/legacy/plugins/upgrade_assistant/index.ts index f1762498246c7..1be728d263372 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/index.ts @@ -43,11 +43,12 @@ export function upgradeAssistant(kibana: any) { init(server: Legacy.Server) { // Add server routes and initialize the plugin here const instance = plugin({} as any); + const { usageCollection } = server.newPlatform.setup.plugins; instance.setup(server.newPlatform.setup.core, { + usageCollection, __LEGACY: { // Legacy objects events: server.events, - usage: server.usage, savedObjects: server.savedObjects, // Legacy functions diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts index 4c378ba25430e..5f95f6e9fd555 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_open_apis.test.ts @@ -15,12 +15,6 @@ import { upsertUIOpenOption } from './es_ui_open_apis'; describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => { const mockIncrementCounter = jest.fn(); const server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { - makeUsageCollector: {}, - register: {}, - }, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts index 26302de74743f..3f2c80f7d6b75 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/es_ui_reindex_apis.test.ts @@ -15,12 +15,6 @@ import { upsertUIReindexOption } from './es_ui_reindex_apis'; describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => { const mockIncrementCounter = jest.fn(); const server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { - makeUsageCollector: {}, - register: {}, - }, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts index 7d1d734748a82..898da4ab0073b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { makeUpgradeAssistantUsageCollector } from './usage_collector'; +export { registerUpgradeAssistantUsageCollector } from './usage_collector'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts index f0553578b86c8..27a0eef0d16f6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as usageCollector from './usage_collector'; +import { registerUpgradeAssistantUsageCollector } from './usage_collector'; /** * Since these route callbacks are so thin, these serve simply as integration tests @@ -16,15 +16,16 @@ describe('Upgrade Assistant Usage Collector', () => { let registerStub: any; let server: any; let callClusterStub: any; + let usageCollection: any; beforeEach(() => { makeUsageCollectorStub = jest.fn(); registerStub = jest.fn(); + usageCollection = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + }; server = jest.fn().mockReturnValue({ - usage: { - collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub }, - register: {}, - }, savedObjects: { getSavedObjectsRepository: jest.fn().mockImplementation(() => { return { @@ -55,20 +56,20 @@ describe('Upgrade Assistant Usage Collector', () => { }); }); - describe('makeUpgradeAssistantUsageCollector', () => { - it('should call collectorSet.register', () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + describe('registerUpgradeAssistantUsageCollector', () => { + it('should registerCollector', () => { + registerUpgradeAssistantUsageCollector(usageCollection, server()); expect(registerStub).toHaveBeenCalledTimes(1); }); it('should call makeUsageCollector with type = upgrade-assistant', () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + registerUpgradeAssistantUsageCollector(usageCollection, server()); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('upgrade-assistant-telemetry'); }); it('fetchUpgradeAssistantMetrics should return correct info', async () => { - usageCollector.makeUpgradeAssistantUsageCollector(server()); + registerUpgradeAssistantUsageCollector(usageCollection, server()); const upgradeAssistantStats = await makeUsageCollectorStub.mock.calls[0][0].fetch( callClusterStub ); diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts index 47a2cd5d51fd4..99c0441063ce6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/lib/telemetry/usage_collector.ts @@ -7,6 +7,7 @@ import { set } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsRepository } from 'src/core/server/saved_objects/service/lib/repository'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { UPGRADE_ASSISTANT_DOC_ID, UPGRADE_ASSISTANT_TYPE, @@ -97,12 +98,15 @@ export async function fetchUpgradeAssistantMetrics( }; } -export function makeUpgradeAssistantUsageCollector(server: ServerShim) { - const upgradeAssistantUsageCollector = server.usage.collectorSet.makeUsageCollector({ +export function registerUpgradeAssistantUsageCollector( + usageCollection: UsageCollectionSetup, + server: ServerShim +) { + const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({ type: UPGRADE_ASSISTANT_TYPE, isReady: () => true, fetch: async (callCluster: any) => fetchUpgradeAssistantMetrics(callCluster, server), }); - server.usage.collectorSet.register(upgradeAssistantUsageCollector); + usageCollection.registerCollector(upgradeAssistantUsageCollector); } diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts index 7bc33142ca321..3d4247ffe70bb 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/plugin.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ServerShim, ServerShimWithRouter } from './types'; import { credentialStoreFactory } from './lib/reindexing/credential_store'; -import { makeUpgradeAssistantUsageCollector } from './lib/telemetry'; +import { registerUpgradeAssistantUsageCollector } from './lib/telemetry'; import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices'; @@ -14,7 +15,10 @@ import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/re import { registerTelemetryRoutes } from './routes/telemetry'; export class UpgradeAssistantServerPlugin implements Plugin { - setup({ http }: CoreSetup, { __LEGACY }: { __LEGACY: ServerShim }) { + setup( + { http }: CoreSetup, + { __LEGACY, usageCollection }: { usageCollection: UsageCollectionSetup; __LEGACY: ServerShim } + ) { const router = http.createRouter(); const shimWithRouter: ServerShimWithRouter = { ...__LEGACY, router }; registerClusterCheckupRoutes(shimWithRouter); @@ -33,7 +37,7 @@ export class UpgradeAssistantServerPlugin implements Plugin const initializerContext = {} as PluginInitializerContext; const { savedObjects } = server; const { elasticsearch, xpack_main } = server.plugins; + const { usageCollection } = server.newPlatform.setup.plugins; + plugin(initializerContext).setup( { route: (arg: any) => server.route(arg), @@ -44,7 +46,7 @@ export const uptime = (kibana: any) => { elasticsearch, savedObjects, - usageCollector: server.usage, + usageCollection, xpack: xpack_main, } ); diff --git a/x-pack/legacy/plugins/uptime/server/kibana.index.ts b/x-pack/legacy/plugins/uptime/server/kibana.index.ts index 874fb2e37e902..73fabc629946b 100644 --- a/x-pack/legacy/plugins/uptime/server/kibana.index.ts +++ b/x-pack/legacy/plugins/uptime/server/kibana.index.ts @@ -22,17 +22,13 @@ export interface KibanaRouteOptions { export interface KibanaServer extends Server { route: (options: KibanaRouteOptions) => void; - usage: { - collectorSet: { - register: (usageCollector: any) => any; - }; - }; } export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => { - const { usageCollector, xpack } = plugins; + const { usageCollection, xpack } = plugins; const libs = compose(server, plugins); - usageCollector.collectorSet.register(KibanaTelemetryAdapter.initUsageCollector(usageCollector)); + KibanaTelemetryAdapter.registerUsageCollector(usageCollection); + initUptimeServer(libs); xpack.registerFeature({ diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index a31b4f99c522a..df2723283f88c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -9,6 +9,7 @@ import { GraphQLSchema } from 'graphql'; import { Lifecycle, ResponseToolkit } from 'hapi'; import { RouteOptions } from 'hapi'; import { SavedObjectsLegacyService } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; export interface UMFrameworkRequest { user: string; @@ -37,7 +38,7 @@ export interface UptimeCoreSetup { export interface UptimeCorePlugins { elasticsearch: any; savedObjects: SavedObjectsLegacyService; - usageCollector: any; + usageCollection: UsageCollectionSetup; xpack: any; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts index d8279cb3399bd..8e4011b4cf0eb 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts @@ -7,21 +7,19 @@ import { KibanaTelemetryAdapter } from '../kibana_telemetry_adapter'; describe('KibanaTelemetryAdapter', () => { - let telemetry: any; + let usageCollection: any; let collector: { type: string; fetch: () => Promise; isReady: () => boolean }; beforeEach(() => { - telemetry = { - collectorSet: { - makeUsageCollector: (val: any) => { - collector = val; - }, + usageCollection = { + makeUsageCollector: (val: any) => { + collector = val; }, }; }); it('collects monitor and overview data', async () => { expect.assertions(1); - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); KibanaTelemetryAdapter.countMonitor(); KibanaTelemetryAdapter.countOverview(); KibanaTelemetryAdapter.countOverview(); @@ -33,7 +31,7 @@ describe('KibanaTelemetryAdapter', () => { expect.assertions(1); // give a time of > 24 hours ago Date.now = jest.fn(() => 1559053560000); - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); KibanaTelemetryAdapter.countMonitor(); KibanaTelemetryAdapter.countOverview(); // give a time of now @@ -47,7 +45,7 @@ describe('KibanaTelemetryAdapter', () => { }); it('defaults ready to `true`', async () => { - KibanaTelemetryAdapter.initUsageCollector(telemetry); + KibanaTelemetryAdapter.initUsageCollector(usageCollection); expect(collector.isReady()).toBe(true); }); }); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index a906c741c5241..8dec0c1d2d485 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; interface UptimeTelemetry { overview_page: number; @@ -19,9 +20,13 @@ const BUCKET_SIZE = 3600; const BUCKET_NUMBER = 24; export class KibanaTelemetryAdapter { - public static initUsageCollector(usageCollector: any) { - const { collectorSet } = usageCollector; - return collectorSet.makeUsageCollector({ + public static registerUsageCollector = (usageCollector: UsageCollectionSetup) => { + const collector = KibanaTelemetryAdapter.initUsageCollector(usageCollector); + usageCollector.registerCollector(collector); + }; + + public static initUsageCollector(usageCollector: UsageCollectionSetup) { + return usageCollector.makeUsageCollector({ type: 'uptime', fetch: async () => { const report = this.getReport(); diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js index d5753ef6f3c85..021464f32a203 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js +++ b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js @@ -23,8 +23,8 @@ export function settingsRoute(server, kbnServer) { const callCluster = (...args) => callWithRequest(req, ...args); // All queries from HTTP API must use authentication headers from the request try { - const { collectorSet } = server.usage; - const settingsCollector = collectorSet.getCollectorByType(KIBANA_SETTINGS_TYPE); + const { usageCollection } = server.newPlatform.setup.plugins; + const settingsCollector = usageCollection.getCollectorByType(KIBANA_SETTINGS_TYPE); let settings = await settingsCollector.fetch(callCluster); if (!settings) { diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 313e4415a8e7c..d806aaf1807ef 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "spaces"], "requiredPlugins": ["features", "licensing"], - "optionalPlugins": ["security", "home"], + "optionalPlugins": ["security", "home", "usageCollection"], "server": true, "ui": false } diff --git a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts similarity index 83% rename from x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts rename to x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts index 912cccbc01782..b343bac9343a3 100644 --- a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSpacesUsageCollector, UsageStats } from './get_spaces_usage_collector'; +import { getSpacesUsageCollector, UsageStats } from './spaces_usage_collector'; import * as Rx from 'rxjs'; import { PluginsSetup } from '../plugin'; import { Feature } from '../../../features/server'; @@ -42,10 +42,8 @@ function setup({ return { licensing, features: featuresSetup, - usage: { - collectorSet: { - makeUsageCollector: (options: any) => new MockUsageCollector(options), - }, + usageCollecion: { + makeUsageCollector: (options: any) => new MockUsageCollector(options), }, }; } @@ -71,10 +69,11 @@ const defaultCallClusterMock = jest.fn().mockResolvedValue({ describe('with a basic license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usage } = setup({ license: { isAvailable: true, type: 'basic' } }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ + const { features, licensing, usageCollecion } = setup({ + license: { isAvailable: true, type: 'basic' }, + }); + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { kibanaIndex: '.kibana', - usage, features, licensing, }); @@ -106,10 +105,9 @@ describe('with a basic license', () => { describe('with no license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usage } = setup({ license: { isAvailable: false } }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ + const { features, licensing, usageCollecion } = setup({ license: { isAvailable: false } }); + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { kibanaIndex: '.kibana', - usage, features, licensing, }); @@ -136,12 +134,11 @@ describe('with no license', () => { describe('with platinum license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usage } = setup({ + const { features, licensing, usageCollecion } = setup({ license: { isAvailable: true, type: 'platinum' }, }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector({ + const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { kibanaIndex: '.kibana', - usage, features, licensing, }); diff --git a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts similarity index 88% rename from x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts rename to x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts index bfbc5e6ab775d..eb6843cfe4538 100644 --- a/x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts @@ -7,6 +7,7 @@ import { get } from 'lodash'; import { CallAPIOptions } from 'src/core/server'; import { take } from 'rxjs/operators'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; // @ts-ignore import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants'; import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants'; @@ -116,7 +117,6 @@ export interface UsageStats { interface CollectorDeps { kibanaIndex: string; - usage: { collectorSet: any }; features: PluginsSetup['features']; licensing: PluginsSetup['licensing']; } @@ -125,9 +125,11 @@ interface CollectorDeps { * @param {Object} server * @return {Object} kibana usage stats type collection object */ -export function getSpacesUsageCollector(deps: CollectorDeps) { - const { collectorSet } = deps.usage; - return collectorSet.makeUsageCollector({ +export function getSpacesUsageCollector( + usageCollection: UsageCollectionSetup, + deps: CollectorDeps +) { + return usageCollection.makeUsageCollector({ type: KIBANA_SPACES_STATS_TYPE, isReady: () => true, fetch: async (callCluster: CallCluster) => { @@ -165,3 +167,14 @@ export function getSpacesUsageCollector(deps: CollectorDeps) { }, }); } + +export function registerSpacesUsageCollector( + usageCollection: UsageCollectionSetup | undefined, + deps: CollectorDeps +) { + if (!usageCollection) { + return; + } + const collector = getSpacesUsageCollector(usageCollection, deps); + usageCollection.registerCollector(collector); +} diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 6511a5dc3f31b..9d45dbb1b748d 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -7,6 +7,8 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { CapabilitiesModifier } from 'src/legacy/server/capabilities'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { HomeServerPluginSetup } from 'src/plugins/home/server'; import { SavedObjectsLegacyService, CoreSetup, @@ -24,25 +26,19 @@ import { AuditLogger } from '../../../../server/lib/audit_logger'; import { spacesSavedObjectsClientWrapperFactory } from './lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { SpacesAuditLogger } from './lib/audit_logger'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; -import { getSpacesUsageCollector } from './lib/get_spaces_usage_collector'; +import { registerSpacesUsageCollector } from './lib/spaces_usage_collector'; import { SpacesService } from './spaces_service'; import { SpacesServiceSetup } from './spaces_service/spaces_service'; import { ConfigType } from './config'; import { toggleUICapabilities } from './lib/toggle_ui_capabilities'; import { initSpacesRequestInterceptors } from './lib/request_interceptors'; import { initExternalSpacesApi } from './routes/api/external'; -import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; /** * Describes a set of APIs that is available in the legacy platform only and required by this plugin * to function properly. */ export interface LegacyAPI { savedObjects: SavedObjectsLegacyService; - usage: { - collectorSet: { - register: (collector: any) => void; - }; - }; tutorial: { addScopedTutorialContextFactory: (factory: any) => void; }; @@ -62,6 +58,7 @@ export interface PluginsSetup { features: FeaturesPluginSetup; licensing: LicensingPluginSetup; security?: SecurityPluginSetup; + usageCollection?: UsageCollectionSetup; home?: HomeServerPluginSetup; } @@ -150,7 +147,12 @@ export class Plugin { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => { this.legacyAPI = legacyAPI; - this.setupLegacyComponents(spacesService, plugins.features, plugins.licensing); + this.setupLegacyComponents( + spacesService, + plugins.features, + plugins.licensing, + plugins.usageCollection + ); }, createDefaultSpace: async () => { const esClient = await core.elasticsearch.adminClient$.pipe(take(1)).toPromise(); @@ -168,7 +170,8 @@ export class Plugin { private setupLegacyComponents( spacesService: SpacesServiceSetup, featuresSetup: FeaturesPluginSetup, - licensingSetup: LicensingPluginSetup + licensingSetup: LicensingPluginSetup, + usageCollectionSetup?: UsageCollectionSetup ) { const legacyAPI = this.getLegacyAPI(); const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects; @@ -180,6 +183,12 @@ export class Plugin { legacyAPI.tutorial.addScopedTutorialContextFactory( createSpacesTutorialContextFactory(spacesService) ); + // Register a function with server to manage the collection of usage stats + registerSpacesUsageCollector(usageCollectionSetup, { + kibanaIndex: legacyAPI.legacyConfig.kibanaIndex, + features: featuresSetup, + licensing: licensingSetup, + }); legacyAPI.capabilities.registerCapabilitiesModifier(async (request, uiCapabilities) => { try { const activeSpace = await spacesService.getActiveSpace(KibanaRequest.from(request)); @@ -189,14 +198,5 @@ export class Plugin { return uiCapabilities; } }); - // Register a function with server to manage the collection of usage stats - legacyAPI.usage.collectorSet.register( - getSpacesUsageCollector({ - kibanaIndex: legacyAPI.legacyConfig.kibanaIndex, - usage: legacyAPI.usage, - features: featuresSetup, - licensing: licensingSetup, - }) - ); } } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts index 38a973c1203d5..62820466b571c 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts @@ -106,7 +106,6 @@ export const createLegacyAPI = ({ auditLogger: {} as any, capabilities: {} as any, tutorial: {} as any, - usage: {} as any, xpackMain: {} as any, savedObjects: savedObjectsService, }; From ce166099a3d74427fa0a3bdb7c5983b3bef0f616 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 26 Nov 2019 19:44:46 -0500 Subject: [PATCH 101/128] [Uptime] Update snapshot counts (#48035) * Add snapshot count function that uses monitor iterator class. * Add js-doc comment. * Add snapshot count route. * Start adding snapshot state management. * Commit changes that were missed in previous. * Finish implementing snapshot count redux code. * Add basePath setter and type to ui state. * Dispatch basePath set action on app render. * Replace GQL-powered Snapshot export with Redux/Rest-powered version. * Extract presentational element to dedicated component. * Update broken test. * Rename action. * Add comments to clarify adapter function. * Remove obsolete code. * Add ui state field to store for tracking when app refreshes. * Make snapshot component refresh on global refresh via redux state. * Remove obsolete @ts-igore. * Alphabetize imports. * Port functional test fixture to REST test. * Delete snapshot GQL query. * Update API test fixtures to match new snapshot count REST response shape. * Update Snapshot count type check to be stricter. * Add tests for Snapshot API call. * Rename new test file from tsx to ts, it has no JSX. * Add tests for snapshot reducer. * Add tests for UI reducer. * Add tests for selectors. * Delete unused test file. * Move snapshot getter and map/reduce logic to dedicated helper function. * Add test for snapshot helper function. * Export type from module. * Rename outdated snapshot file. * Add action creator for fetch success. * Reorganize ui actions file. * Update snapshot effect to put error when input params are not valid. * Simplify typing code for a function. * Simplify snapshot count reduction. * Rename a function. * Rewrite a function to increase code clarity. * Remove duplicated interface. * Add very high ceiling for snapshot count iteration. * Update broken test assertion. --- .../common/constants/context_defaults.ts | 6 + .../uptime/common/runtime_types/index.ts | 1 + .../common/runtime_types/snapshot/index.ts | 7 + .../runtime_types/snapshot/snapshot_count.ts | 16 ++ .../functional/__tests__/snapshot.test.tsx | 18 +- .../public/components/functional/snapshot.tsx | 118 ++++++++++--- .../components/functional/status_panel.tsx | 16 +- .../functional/uptime_date_picker.tsx | 1 - .../plugins/uptime/public/pages/overview.tsx | 4 + .../plugins/uptime/public/queries/index.ts | 1 - .../uptime/public/queries/snapshot_query.ts | 34 ---- .../uptime/public/state/actions/index.ts | 1 + .../uptime/public/state/actions/snapshot.ts | 62 +++++++ .../plugins/uptime/public/state/actions/ui.ts | 24 ++- .../__snapshots__/snapshot.test.ts.snap | 8 + .../state/api/__tests__/snapshot.test.ts | 73 +++++++++ .../plugins/uptime/public/state/api/index.ts | 2 + .../uptime/public/state/api/snapshot.ts | 46 ++++++ .../uptime/public/state/effects/index.ts | 2 + .../uptime/public/state/effects/snapshot.ts | 45 +++++ .../plugins/uptime/public/state/index.ts | 4 +- .../__snapshots__/snapshot.test.ts.snap | 55 +++++++ .../__tests__/__snapshots__/ui.test.ts.snap | 28 ++++ .../state/reducers/__tests__/snapshot.test.ts | 64 ++++++++ .../state/reducers/__tests__/ui.test.ts | 55 +++++++ .../uptime/public/state/reducers/index.ts | 6 +- .../uptime/public/state/reducers/snapshot.ts | 53 ++++++ .../uptime/public/state/reducers/ui.ts | 8 + .../state/selectors/__tests__/index.test.ts | 42 +++++ .../uptime/public/state/selectors/index.ts | 6 +- .../plugins/uptime/public/uptime_app.tsx | 6 +- .../server/graphql/monitors/resolvers.ts | 27 --- .../server/graphql/monitors/schema.gql.ts | 18 -- .../get_snapshot_helper.test.ts.snap | 10 ++ .../__tests__/example_filter.json | 82 --------- .../__tests__/get_snapshot_helper.test.ts | 106 ++++++++++++ .../adapters/monitor_states/adapter_types.ts | 7 + .../elasticsearch_monitor_states_adapter.ts | 23 +++ .../monitor_states/get_snapshot_helper.ts | 40 +++++ .../adapters/monitor_states/search/index.ts | 1 + .../search/monitor_group_iterator.ts | 9 +- .../lib/adapters/monitors/adapter_types.ts | 7 - .../elasticsearch_monitors_adapter.ts | 155 +----------------- .../plugins/uptime/server/rest_api/index.ts | 8 +- .../rest_api/snapshot/get_snapshot_count.ts | 35 ++++ .../uptime/server/rest_api/snapshot/index.ts | 7 + .../uptime/graphql/fixtures/snapshot.json | 12 +- .../graphql/fixtures/snapshot_empty.json | 12 +- .../fixtures/snapshot_filtered_by_down.json | 12 +- .../fixtures/snapshot_filtered_by_up.json | 12 +- .../apis/uptime/graphql/index.js | 1 - .../apis/uptime/graphql/snapshot.js | 101 ------------ .../test/api_integration/apis/uptime/index.js | 1 + .../api_integration/apis/uptime/rest/index.ts | 16 ++ .../apis/uptime/rest/snapshot.ts | 52 ++++++ 55 files changed, 1057 insertions(+), 509 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts create mode 100644 x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts delete mode 100644 x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts create mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap delete mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json create mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts create mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts create mode 100644 x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts create mode 100644 x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts delete mode 100644 x-pack/test/api_integration/apis/uptime/graphql/snapshot.js create mode 100644 x-pack/test/api_integration/apis/uptime/rest/index.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts b/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts index 3a42df8c5e9ab..4c32769d73e84 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts @@ -35,4 +35,10 @@ export const CONTEXT_DEFAULTS = { cursorDirection: CursorDirection.AFTER, sortOrder: SortOrder.ASC, }, + + /** + * Defines the maximum number of monitors to iterate on + * in a single count session. The intention is to catch as many as possible. + */ + MAX_MONITORS_FOR_SNAPSHOT_COUNT: 1000000, }; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts index 3a5d0549c5d45..a88e28f2e5a09 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './snapshot'; export * from './monitor/monitor_details'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts new file mode 100644 index 0000000000000..99bf783d3ab2e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Snapshot, SnapshotType } from './snapshot_count'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts new file mode 100644 index 0000000000000..d4935c50ff5b8 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const SnapshotType = t.type({ + down: t.number, + mixed: t.number, + total: t.number, + up: t.number, +}); + +export type Snapshot = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx index 40e3daae67185..193f37c8fe56b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx @@ -6,21 +6,19 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Snapshot as SnapshotType } from '../../../../common/graphql/types'; -import { SnapshotComponent } from '../snapshot'; +import { Snapshot } from '../../../../common/runtime_types'; +import { PresentationalComponent } from '../snapshot'; describe('Snapshot component', () => { - const snapshot: SnapshotType = { - counts: { - up: 8, - down: 2, - mixed: 0, - total: 10, - }, + const snapshot: Snapshot = { + up: 8, + down: 2, + mixed: 0, + total: 10, }; it('renders without errors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index ddc6df14c2ade..e0d282a5348a0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -5,46 +5,69 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect } from 'react'; import { get } from 'lodash'; +import { connect } from 'react-redux'; +import { Snapshot as SnapshotType } from '../../../common/runtime_types'; import { DonutChart } from './charts'; -import { Snapshot as SnapshotType } from '../../../common/graphql/types'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order'; -import { snapshotQuery } from '../../queries'; +import { fetchSnapshotCount } from '../../state/actions'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; +import { AppState } from '../../state'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; -interface SnapshotQueryResult { - snapshot?: SnapshotType; -} - -interface SnapshotProps { +/** + * Props expected from parent components. + */ +interface OwnProps { + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; /** * Height is needed, since by default charts takes height of 100% */ height?: string; + statusFilter?: string; } -export type SnapshotComponentProps = SnapshotProps & UptimeGraphQLQueryProps; +/** + * Props given by the Redux store based on action input. + */ +interface StoreProps { + count: SnapshotType; + lastRefresh: number; + loading: boolean; +} /** - * This component visualizes a KPI and histogram chart to help users quickly - * glean the status of their uptime environment. - * @param props the props required by the component + * Contains functions that will dispatch actions used + * for this component's lifecyclel + */ +interface DispatchProps { + loadSnapshotCount: typeof fetchSnapshotCount; +} + +/** + * Props used to render the Snapshot component. */ -export const SnapshotComponent = ({ data, loading, height }: SnapshotComponentProps) => ( +type Props = OwnProps & StoreProps & DispatchProps; + +type PresentationalComponentProps = Pick & + Pick; + +export const PresentationalComponent: React.FC = ({ + count, + height, + loading, +}) => ( - (data, 'snapshot.counts.down', 0)} - total={get(data, 'snapshot.counts.total', 0)} - /> + (count, 'down', 0)} total={get(count, 'total', 0)} /> (data, 'snapshot.counts.up', 0)} - down={get(data, 'snapshot.counts.down', 0)} + up={get(count, 'up', 0)} + down={get(count, 'down', 0)} height={SNAPSHOT_CHART_HEIGHT} width={SNAPSHOT_CHART_WIDTH} /> @@ -54,8 +77,55 @@ export const SnapshotComponent = ({ data, loading, height }: SnapshotComponentPr /** * This component visualizes a KPI and histogram chart to help users quickly * glean the status of their uptime environment. + * @param props the props required by the component */ -export const Snapshot = withUptimeGraphQL( - SnapshotComponent, - snapshotQuery -); +export const Container: React.FC = ({ + count, + dateRangeStart, + dateRangeEnd, + filters, + height, + statusFilter, + lastRefresh, + loading, + loadSnapshotCount, +}: Props) => { + useEffect(() => { + loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); + }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, statusFilter]); + return ; +}; + +/** + * Provides state to connected component. + * @param state the root app state + */ +const mapStateToProps = ({ + snapshot: { count, loading }, + ui: { lastRefresh }, +}: AppState): StoreProps => ({ + count, + lastRefresh, + loading, +}); + +/** + * Used for fetching snapshot counts. + * @param dispatch redux-provided action dispatcher + */ +const mapDispatchToProps = (dispatch: any) => ({ + loadSnapshotCount: ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): DispatchProps => { + return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); + }, +}); + +export const Snapshot = connect( + // @ts-ignore connect is expecting null | undefined for some reason + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx index a58d06ece0ede..b74bc943dc3eb 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx @@ -12,6 +12,10 @@ import { Snapshot } from './snapshot'; interface StatusPanelProps { absoluteDateRangeStart: number; absoluteDateRangeEnd: number; + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; sharedProps: { [key: string]: any }; } @@ -20,12 +24,22 @@ const STATUS_CHART_HEIGHT = '160px'; export const StatusPanel = ({ absoluteDateRangeStart, absoluteDateRangeEnd, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, sharedProps, }: StatusPanelProps) => ( - + { updateUrl({ dateRangeStart: start, dateRangeEnd: end }); refreshApp(); }} - // @ts-ignore onRefresh is not defined on EuiSuperDatePicker's type yet onRefresh={refreshApp} onRefreshChange={({ isPaused, refreshInterval }: SuperDateRangePickerRefreshChangedEvent) => { updateUrl({ diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index 09d40d32b696c..561cc934a9b76 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -150,6 +150,10 @@ export const OverviewPage = ({ diff --git a/x-pack/legacy/plugins/uptime/public/queries/index.ts b/x-pack/legacy/plugins/uptime/public/queries/index.ts index d680ec6c543c4..b86522c03aba8 100644 --- a/x-pack/legacy/plugins/uptime/public/queries/index.ts +++ b/x-pack/legacy/plugins/uptime/public/queries/index.ts @@ -10,4 +10,3 @@ export { monitorChartsQuery, monitorChartsQueryString } from './monitor_charts_q export { monitorPageTitleQuery } from './monitor_page_title_query'; export { monitorStatusBarQuery, monitorStatusBarQueryString } from './monitor_status_bar_query'; export { pingsQuery, pingsQueryString } from './pings_query'; -export { snapshotQuery, snapshotQueryString } from './snapshot_query'; diff --git a/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts b/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts deleted file mode 100644 index 2db226876d220..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/queries/snapshot_query.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const snapshotQueryString = ` -query Snapshot( - $dateRangeStart: String! - $dateRangeEnd: String! - $filters: String - $statusFilter: String -) { - snapshot: getSnapshot( - dateRangeStart: $dateRangeStart - dateRangeEnd: $dateRangeEnd - filters: $filters - statusFilter: $statusFilter - ) { - counts { - down - mixed - up - total - } - } -} -`; - -export const snapshotQuery = gql` - ${snapshotQueryString} -`; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts index 1a33812ca8566..6b896b07bb066 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './snapshot'; export * from './ui'; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts new file mode 100644 index 0000000000000..fe87a6a5960ee --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Snapshot } from '../../../common/runtime_types'; +export const FETCH_SNAPSHOT_COUNT = 'FETCH_SNAPSHOT_COUNT'; +export const FETCH_SNAPSHOT_COUNT_FAIL = 'FETCH_SNAPSHOT_COUNT_FAIL'; +export const FETCH_SNAPSHOT_COUNT_SUCCESS = 'FETCH_SNAPSHOT_COUNT_SUCCESS'; + +export interface GetSnapshotPayload { + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; +} + +interface GetSnapshotCountFetchAction { + type: typeof FETCH_SNAPSHOT_COUNT; + payload: GetSnapshotPayload; +} + +interface GetSnapshotCountSuccessAction { + type: typeof FETCH_SNAPSHOT_COUNT_SUCCESS; + payload: Snapshot; +} + +interface GetSnapshotCountFailAction { + type: typeof FETCH_SNAPSHOT_COUNT_FAIL; + payload: Error; +} + +export type SnapshotActionTypes = + | GetSnapshotCountFetchAction + | GetSnapshotCountSuccessAction + | GetSnapshotCountFailAction; + +export const fetchSnapshotCount = ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string +): GetSnapshotCountFetchAction => ({ + type: FETCH_SNAPSHOT_COUNT, + payload: { + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, + }, +}); + +export const fetchSnapshotCountFail = (error: Error): GetSnapshotCountFailAction => ({ + type: FETCH_SNAPSHOT_COUNT_FAIL, + payload: error, +}); + +export const fetchSnapshotCountSuccess = (snapshot: Snapshot) => ({ + type: FETCH_SNAPSHOT_COUNT_SUCCESS, + payload: snapshot, +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts index f0234f903d3d8..0bb2d8447419b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts @@ -6,22 +6,33 @@ export const SET_INTEGRATION_POPOVER_STATE = 'SET_INTEGRATION_POPOVER_STATE'; export const SET_BASE_PATH = 'SET_BASE_PATH'; +export const REFRESH_APP = 'REFRESH_APP'; export interface PopoverState { id: string; open: boolean; } +interface SetBasePathAction { + type: typeof SET_BASE_PATH; + payload: string; +} + interface SetIntegrationPopoverAction { type: typeof SET_INTEGRATION_POPOVER_STATE; payload: PopoverState; } -interface SetBasePathAction { - type: typeof SET_BASE_PATH; - payload: string; +interface TriggerAppRefreshAction { + type: typeof REFRESH_APP; + payload: number; } +export type UiActionTypes = + | SetIntegrationPopoverAction + | SetBasePathAction + | TriggerAppRefreshAction; + export function toggleIntegrationsPopover(popoverState: PopoverState): SetIntegrationPopoverAction { return { type: SET_INTEGRATION_POPOVER_STATE, @@ -36,4 +47,9 @@ export function setBasePath(basePath: string): SetBasePathAction { }; } -export type UiActionTypes = SetIntegrationPopoverAction | SetBasePathAction; +export function triggerAppRefresh(refreshTime: number): TriggerAppRefreshAction { + return { + type: REFRESH_APP, + payload: refreshTime, + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000000..53716681664c2 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot API throws when server response doesn't correspond to expected type 1`] = ` +[Error: Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/down: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/mixed: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/total: number +Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/up: number] +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts new file mode 100644 index 0000000000000..f5fdfb172bc58 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fetchSnapshotCount } from '../snapshot'; + +describe('snapshot API', () => { + let fetchMock: jest.SpyInstance>>; + let mockResponse: Partial; + + beforeEach(() => { + fetchMock = jest.spyOn(window, 'fetch'); + mockResponse = { + ok: true, + json: () => new Promise(r => r({ up: 3, down: 12, mixed: 0, total: 15 })), + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls url with expected params and returns response body on 200', async () => { + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + const resp = await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'monitor.id:"auto-http-0X21EE76EAC459873F"', + statusFilter: 'up', + }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/uptime/snapshot/count?dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%22auto-http-0X21EE76EAC459873F%22&statusFilter=up' + ); + expect(resp).toEqual({ up: 3, down: 12, mixed: 0, total: 15 }); + }); + + it(`throws when server response doesn't correspond to expected type`, async () => { + mockResponse = { ok: true, json: () => new Promise(r => r({ foo: 'bar' })) }; + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + let error: Error | undefined; + try { + await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'monitor.id: baz', + statusFilter: 'up', + }); + } catch (e) { + error = e; + } + expect(error).toMatchSnapshot(); + }); + + it('throws an error when response is not ok', async () => { + mockResponse = { ok: false, statusText: 'There was an error fetching your data.' }; + fetchMock.mockReturnValue(new Promise(r => r(mockResponse))); + let error: Error | undefined; + try { + await fetchSnapshotCount({ + basePath: '', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + }); + } catch (e) { + error = e; + } + expect(error).toEqual(new Error('There was an error fetching your data.')); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/legacy/plugins/uptime/public/state/api/index.ts index e9b8082b417ba..a4429868494f1 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/index.ts @@ -3,4 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + export * from './monitor'; +export * from './snapshot'; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts new file mode 100644 index 0000000000000..cbfe00a4a8746 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { getApiPath } from '../../lib/helper'; +import { SnapshotType, Snapshot } from '../../../common/runtime_types'; + +interface ApiRequest { + basePath: string; + dateRangeStart: string; + dateRangeEnd: string; + filters?: string; + statusFilter?: string; +} + +export const fetchSnapshotCount = async ({ + basePath, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, +}: ApiRequest): Promise => { + const url = getApiPath(`/api/uptime/snapshot/count`, basePath); + const params = { + dateRangeStart, + dateRangeEnd, + ...(filters && { filters }), + ...(statusFilter && { statusFilter }), + }; + const urlParams = new URLSearchParams(params).toString(); + const response = await fetch(`${url}?${urlParams}`); + if (!response.ok) { + throw new Error(response.statusText); + } + const responseData = await response.json(); + const decoded = SnapshotType.decode(responseData); + ThrowReporter.report(decoded); + if (isRight(decoded)) { + return decoded.right; + } + throw new Error('`getSnapshotCount` response did not correspond to expected type'); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts index 92802f2e0c84a..4eb027d642974 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts @@ -6,7 +6,9 @@ import { fork } from 'redux-saga/effects'; import { fetchMonitorDetailsEffect } from './monitor'; +import { fetchSnapshotCountSaga } from './snapshot'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); + yield fork(fetchSnapshotCountSaga); } diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts new file mode 100644 index 0000000000000..23ac1016d2244 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { call, put, takeLatest, select } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { + FETCH_SNAPSHOT_COUNT, + GetSnapshotPayload, + fetchSnapshotCountFail, + fetchSnapshotCountSuccess, +} from '../actions'; +import { fetchSnapshotCount } from '../api'; +import { getBasePath } from '../selectors'; + +function* snapshotSaga(action: Action) { + try { + if (!action.payload) { + yield put( + fetchSnapshotCountFail(new Error('Cannot fetch snapshot for undefined parameters.')) + ); + return; + } + const { + payload: { dateRangeStart, dateRangeEnd, filters, statusFilter }, + } = action; + const basePath = yield select(getBasePath); + const response = yield call(fetchSnapshotCount, { + basePath, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, + }); + yield put(fetchSnapshotCountSuccess(response)); + } catch (error) { + yield put(fetchSnapshotCountFail(error)); + } +} + +export function* fetchSnapshotCountSaga() { + yield takeLatest(FETCH_SNAPSHOT_COUNT, snapshotSaga); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/index.ts b/x-pack/legacy/plugins/uptime/public/state/index.ts index 01cffb636d33c..e3563c74294d2 100644 --- a/x-pack/legacy/plugins/uptime/public/state/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/index.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { compose, createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; - -import { rootReducer } from './reducers'; import { rootEffect } from './effects'; +import { rootReducer } from './reducers'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000000..d3a21ec9eece3 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snapshot reducer appends a current error to existing errors list 1`] = ` +Object { + "count": Object { + "down": 0, + "mixed": 0, + "total": 0, + "up": 0, + }, + "errors": Array [ + [Error: I couldn't get your data because the server denied the request], + ], + "loading": false, +} +`; + +exports[`snapshot reducer changes the count when a snapshot fetch succeeds 1`] = ` +Object { + "count": Object { + "down": 15, + "mixed": 0, + "total": 25, + "up": 10, + }, + "errors": Array [], + "loading": false, +} +`; + +exports[`snapshot reducer sets the state's status to loading during a fetch 1`] = ` +Object { + "count": Object { + "down": 0, + "mixed": 0, + "total": 0, + "up": 0, + }, + "errors": Array [], + "loading": true, +} +`; + +exports[`snapshot reducer updates existing state 1`] = ` +Object { + "count": Object { + "down": 1, + "mixed": 0, + "total": 4, + "up": 3, + }, + "errors": Array [], + "loading": true, +} +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap new file mode 100644 index 0000000000000..75516da18c633 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ui reducer adds integration popover status to state 1`] = ` +Object { + "basePath": "", + "integrationsPopoverOpen": Object { + "id": "popover-2", + "open": true, + }, + "lastRefresh": 125, +} +`; + +exports[`ui reducer sets the application's base path 1`] = ` +Object { + "basePath": "yyz", + "integrationsPopoverOpen": null, + "lastRefresh": 125, +} +`; + +exports[`ui reducer updates the refresh value 1`] = ` +Object { + "basePath": "", + "integrationsPopoverOpen": null, + "lastRefresh": 125, +} +`; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts new file mode 100644 index 0000000000000..a4b317d5af197 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { snapshotReducer } from '../snapshot'; +import { SnapshotActionTypes } from '../../actions'; + +describe('snapshot reducer', () => { + it('updates existing state', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT', + payload: { + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + filters: 'foo: bar', + statusFilter: 'up', + }, + }; + expect( + snapshotReducer( + { + count: { down: 1, mixed: 0, total: 4, up: 3 }, + errors: [], + loading: false, + }, + action + ) + ).toMatchSnapshot(); + }); + + it(`sets the state's status to loading during a fetch`, () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT', + payload: { + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + }, + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); + + it('changes the count when a snapshot fetch succeeds', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT_SUCCESS', + payload: { + up: 10, + down: 15, + mixed: 0, + total: 25, + }, + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); + + it('appends a current error to existing errors list', () => { + const action: SnapshotActionTypes = { + type: 'FETCH_SNAPSHOT_COUNT_FAIL', + payload: new Error(`I couldn't get your data because the server denied the request`), + }; + expect(snapshotReducer(undefined, action)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts new file mode 100644 index 0000000000000..9be863f0b700d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UiActionTypes } from '../../actions'; +import { uiReducer } from '../ui'; + +describe('ui reducer', () => { + it(`sets the application's base path`, () => { + const action: UiActionTypes = { + type: 'SET_BASE_PATH', + payload: 'yyz', + }; + expect( + uiReducer( + { + basePath: 'abc', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + action + ) + ).toMatchSnapshot(); + }); + + it('adds integration popover status to state', () => { + const action: UiActionTypes = { + type: 'SET_INTEGRATION_POPOVER_STATE', + payload: { + id: 'popover-2', + open: true, + }, + }; + expect( + uiReducer( + { + basePath: '', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + action + ) + ).toMatchSnapshot(); + }); + + it('updates the refresh value', () => { + const action: UiActionTypes = { + type: 'REFRESH_APP', + payload: 125, + }; + expect(uiReducer(undefined, action)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts index 186b02395b779..f0c3d1c2cbecf 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts @@ -5,10 +5,12 @@ */ import { combineReducers } from 'redux'; -import { uiReducer } from './ui'; import { monitorReducer } from './monitor'; +import { snapshotReducer } from './snapshot'; +import { uiReducer } from './ui'; export const rootReducer = combineReducers({ - ui: uiReducer, monitor: monitorReducer, + snapshot: snapshotReducer, + ui: uiReducer, }); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts new file mode 100644 index 0000000000000..dd9449325f4fb --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Snapshot } from '../../../common/runtime_types'; +import { + FETCH_SNAPSHOT_COUNT, + FETCH_SNAPSHOT_COUNT_FAIL, + FETCH_SNAPSHOT_COUNT_SUCCESS, + SnapshotActionTypes, +} from '../actions'; + +export interface SnapshotState { + count: Snapshot; + errors: any[]; + loading: boolean; +} + +const initialState: SnapshotState = { + count: { + down: 0, + mixed: 0, + total: 0, + up: 0, + }, + errors: [], + loading: false, +}; + +export function snapshotReducer(state = initialState, action: SnapshotActionTypes): SnapshotState { + switch (action.type) { + case FETCH_SNAPSHOT_COUNT: + return { + ...state, + loading: true, + }; + case FETCH_SNAPSHOT_COUNT_SUCCESS: + return { + ...state, + count: action.payload, + loading: false, + }; + case FETCH_SNAPSHOT_COUNT_FAIL: + return { + ...state, + errors: [...state.errors, action.payload], + }; + default: + return state; + } +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts index d095d6ba961ca..be95c8fff6bec 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts @@ -9,20 +9,28 @@ import { PopoverState, SET_INTEGRATION_POPOVER_STATE, SET_BASE_PATH, + REFRESH_APP, } from '../actions/ui'; export interface UiState { integrationsPopoverOpen: PopoverState | null; basePath: string; + lastRefresh: number; } const initialState: UiState = { integrationsPopoverOpen: null, basePath: '', + lastRefresh: Date.now(), }; export function uiReducer(state = initialState, action: UiActionTypes): UiState { switch (action.type) { + case REFRESH_APP: + return { + ...state, + lastRefresh: action.payload, + }; case SET_INTEGRATION_POPOVER_STATE: const popoverState = action.payload; return { diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts new file mode 100644 index 0000000000000..70cd2b19860ba --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getBasePath, isIntegrationsPopupOpen } from '../index'; +import { AppState } from '../../../state'; + +describe('state selectors', () => { + const state: AppState = { + monitor: { + monitorDetailsList: [], + loading: false, + errors: [], + }, + snapshot: { + count: { + up: 2, + down: 0, + mixed: 0, + total: 2, + }, + errors: [], + loading: false, + }, + ui: { basePath: 'yyz', integrationsPopoverOpen: null, lastRefresh: 125 }, + }; + + it('selects base path from state', () => { + expect(getBasePath(state)).toBe('yyz'); + }); + + it('gets integrations popup state', () => { + const integrationsPopupOpen = { + id: 'popup-id', + open: true, + }; + state.ui.integrationsPopoverOpen = integrationsPopupOpen; + expect(isIntegrationsPopupOpen(state)).toBe(integrationsPopupOpen); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 59c3f0c31539f..245b45a939950 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -3,11 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { AppState } from '../../state'; -export const isIntegrationsPopupOpen = (state: AppState) => state.ui.integrationsPopoverOpen; +export const getBasePath = ({ ui: { basePath } }: AppState) => basePath; -export const getBasePath = (state: AppState) => state.ui.basePath; +export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: AppState) => + integrationsPopoverOpen; export const getMonitorDetails = (state: AppState, summary: any) => { return state.monitor.monitorDetailsList[summary.monitor_id]; diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index 3fdceb8b1b2bd..47743729c1e76 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -22,7 +22,7 @@ import { UptimeDatePicker } from './components/functional/uptime_date_picker'; import { useUrlParams } from './hooks'; import { getTitle } from './lib/helper/get_title'; import { store } from './state'; -import { setBasePath } from './state/actions'; +import { setBasePath, triggerAppRefresh } from './state/actions'; export interface UptimeAppColors { danger: string; @@ -116,7 +116,9 @@ const Application = (props: UptimeAppProps) => { }, []); const refreshApp = () => { - setLastRefresh(Date.now()); + const refreshTime = Date.now(); + setLastRefresh(refreshTime); + store.dispatch(triggerAppRefresh(refreshTime)); }; const [getUrlParams] = useUrlParams(); diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts index 96a386b6a6848..415afc87e201e 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts @@ -12,24 +12,15 @@ import { GetLatestMonitorsQueryArgs, GetMonitorChartsDataQueryArgs, GetMonitorPageTitleQueryArgs, - GetSnapshotQueryArgs, MonitorChart, MonitorPageTitle, Ping, - Snapshot, GetSnapshotHistogramQueryArgs, } from '../../../common/graphql/types'; import { UMServerLibs } from '../../lib/lib'; import { CreateUMGraphQLResolvers, UMContext } from '../types'; import { HistogramResult } from '../../../common/domain_types'; -export type UMSnapshotResolver = UMResolver< - Snapshot | Promise, - any, - GetSnapshotQueryArgs, - UMContext ->; - export type UMMonitorsResolver = UMResolver, any, UMGqlRange, UMContext>; export type UMLatestMonitorsResolver = UMResolver< @@ -71,7 +62,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( libs: UMServerLibs ): { Query: { - getSnapshot: UMSnapshotResolver; getSnapshotHistogram: UMGetSnapshotHistogram; getMonitorChartsData: UMGetMonitorChartsResolver; getLatestMonitors: UMLatestMonitorsResolver; @@ -80,23 +70,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( }; } => ({ Query: { - async getSnapshot( - resolver, - { dateRangeStart, dateRangeEnd, filters, statusFilter }, - { req } - ): Promise { - const counts = await libs.monitors.getSnapshotCount( - req, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter - ); - - return { - counts, - }; - }, async getSnapshotHistogram( resolver, { dateRangeStart, dateRangeEnd, filters, monitorId, statusFilter }, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts index 97dcbd12fff2e..f9b14c63e70bb 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/schema.gql.ts @@ -31,17 +31,6 @@ export const monitorsSchema = gql` y: UnsignedInteger } - type SnapshotCount { - up: Int! - down: Int! - mixed: Int! - total: Int! - } - - type Snapshot { - counts: SnapshotCount! - } - type DataPoint { x: UnsignedInteger y: Float @@ -139,13 +128,6 @@ export const monitorsSchema = gql` statusFilter: String ): LatestMonitorsResult - getSnapshot( - dateRangeStart: String! - dateRangeEnd: String! - filters: String - statusFilter: String - ): Snapshot - getSnapshotHistogram( dateRangeStart: String! dateRangeEnd: String! diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap new file mode 100644 index 0000000000000..29c82ff455d36 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/__snapshots__/get_snapshot_helper.test.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`get snapshot helper reduces check groups as expected 1`] = ` +Object { + "down": 1, + "mixed": 0, + "total": 3, + "up": 2, +} +`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json deleted file mode 100644 index bd4248755095d..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/example_filter.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "bool": { - "filter": [ - { - "bool": { - "filter": [ - { - "bool": { - "should": [{ "match_phrase": { "monitor.id": "green-0001" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [{ "match_phrase": { "monitor.name": "" } }], - "minimum_should_match": 1 - } - } - ] - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0000" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0001" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0002" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [ - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0003" } }], - "minimum_should_match": 1 - } - }, - { - "bool": { - "should": [{ "match": { "monitor.id": "green-0004" } }], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ], - "minimum_should_match": 1 - } - } - ] - } -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts new file mode 100644 index 0000000000000..917e4a149de67 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/get_snapshot_helper.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSnapshotCountHelper } from '../get_snapshot_helper'; +import { MonitorGroups } from '../search'; + +describe('get snapshot helper', () => { + let mockIterator: any; + beforeAll(() => { + mockIterator = jest.fn(); + const summaryTimestamp = new Date('2019-01-01'); + const firstResult: MonitorGroups = { + id: 'firstGroup', + groups: [ + { + monitorId: 'first-monitor', + location: 'us-east-1', + checkGroup: 'abc', + status: 'down', + summaryTimestamp, + }, + { + monitorId: 'first-monitor', + location: 'us-west-1', + checkGroup: 'abc', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'first-monitor', + location: 'amsterdam', + checkGroup: 'abc', + status: 'down', + summaryTimestamp, + }, + ], + }; + const secondResult: MonitorGroups = { + id: 'secondGroup', + groups: [ + { + monitorId: 'second-monitor', + location: 'us-east-1', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'second-monitor', + location: 'us-west-1', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'second-monitor', + location: 'amsterdam', + checkGroup: 'yyz', + status: 'up', + summaryTimestamp, + }, + ], + }; + const thirdResult: MonitorGroups = { + id: 'thirdGroup', + groups: [ + { + monitorId: 'third-monitor', + location: 'us-east-1', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'third-monitor', + location: 'us-west-1', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + { + monitorId: 'third-monitor', + location: 'amsterdam', + checkGroup: 'dt', + status: 'up', + summaryTimestamp, + }, + ], + }; + + const mockNext = jest + .fn() + .mockReturnValueOnce(firstResult) + .mockReturnValueOnce(secondResult) + .mockReturnValueOnce(thirdResult) + .mockReturnValueOnce(null); + mockIterator.next = mockNext; + }); + + it('reduces check groups as expected', async () => { + expect(await getSnapshotCountHelper(mockIterator)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts index 781f30314d350..57b1744f5d324 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts @@ -21,6 +21,13 @@ export interface UMMonitorStatesAdapter { statusFilter?: string | null ): Promise; statesIndexExists(request: any): Promise; + getSnapshotCount( + request: any, + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): Promise; } export interface CursorPagination { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts index 59c3e022e7d04..c3593854fa53f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts @@ -9,6 +9,9 @@ import { UMMonitorStatesAdapter, GetMonitorStatesResult, CursorPagination } from import { StatesIndexStatus } from '../../../../common/graphql/types'; import { INDEX_NAMES, CONTEXT_DEFAULTS } from '../../../../common/constants'; import { fetchPage } from './search'; +import { MonitorGroupIterator } from './search/monitor_group_iterator'; +import { Snapshot } from '../../../../common/runtime_types'; +import { getSnapshotCountHelper } from './get_snapshot_helper'; export interface QueryContext { database: any; @@ -57,6 +60,26 @@ export class ElasticsearchMonitorStatesAdapter implements UMMonitorStatesAdapter }; } + public async getSnapshotCount( + request: any, + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): Promise { + const context: QueryContext = { + database: this.database, + request, + dateRangeStart, + dateRangeEnd, + pagination: CONTEXT_DEFAULTS.CURSOR_PAGINATION, + filterClause: filters && filters !== '' ? JSON.parse(filters) : null, + size: CONTEXT_DEFAULTS.MAX_MONITORS_FOR_SNAPSHOT_COUNT, + statusFilter, + }; + return getSnapshotCountHelper(new MonitorGroupIterator(context)); + } + public async statesIndexExists(request: any): Promise { // TODO: adapt this to the states index in future release const { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts new file mode 100644 index 0000000000000..8bd21b77406df --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/get_snapshot_helper.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MonitorGroups, MonitorGroupIterator } from './search'; +import { Snapshot } from '../../../../common/runtime_types'; + +const reduceItemsToCounts = (items: MonitorGroups[]) => { + let down = 0; + let up = 0; + items.forEach(item => { + if (item.groups.some(group => group.status === 'down')) { + down++; + } else { + up++; + } + }); + return { + down, + mixed: 0, + total: down + up, + up, + }; +}; + +export const getSnapshotCountHelper = async (iterator: MonitorGroupIterator): Promise => { + const items: MonitorGroups[] = []; + let res: MonitorGroups | null; + // query the index to find the most recent check group for each monitor/location + do { + res = await iterator.next(); + if (res) { + items.push(res); + } + } while (res !== null); + + return reduceItemsToCounts(items); +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts index 2fa2112161dcd..040c256935692 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts @@ -5,3 +5,4 @@ */ export { fetchPage, MonitorGroups, MonitorLocCheckGroup, MonitorGroupsPage } from './fetch_page'; +export { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts index 2fec58593e5d8..1de2dbb0e364d 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts @@ -97,9 +97,12 @@ export class MonitorGroupIterator { } } - // Attempts to buffer more results fetching a single chunk. - // If trim is set to true, which is the default, it will delete all items in the buffer prior to the current item. - // to free up space. + /** + * Attempts to buffer more results fetching a single chunk. + * If trim is set to true, which is the default, it will delete all items in the buffer prior to the current item. + * to free up space. + * @param size the number of items to chunk + */ async attemptBufferMore( size: number = CHUNK_SIZE ): Promise<{ hasMore: boolean; gotHit: boolean }> { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts index 996e80d2c8613..f6ac587b0ceec 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts @@ -14,13 +14,6 @@ export interface UMMonitorsAdapter { dateRangeEnd: string, location?: string | null ): Promise; - getSnapshotCount( - request: any, - dateRangeStart: string, - dateRangeEnd: string, - filters?: string | null, - statusFilter?: string | null - ): Promise; getFilterBar(request: any, dateRangeStart: string, dateRangeEnd: string): Promise; getMonitorPageTitle(request: any, monitorId: string): Promise; getMonitorDetails(request: any, monitorId: string): Promise; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts index 1a391e90f2a5e..ef0837a043172 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set, reduce } from 'lodash'; -import { INDEX_NAMES, QUERY } from '../../../../common/constants'; +import { get } from 'lodash'; +import { INDEX_NAMES } from '../../../../common/constants'; import { FilterBar, MonitorChart, @@ -13,7 +13,7 @@ import { Ping, LocationDurationLine, } from '../../../../common/graphql/types'; -import { getFilterClause, parseFilterQuery, getHistogramIntervalFormatted } from '../../helper'; +import { getHistogramIntervalFormatted } from '../../helper'; import { DatabaseAdapter } from '../database'; import { UMMonitorsAdapter } from './adapter_types'; import { MonitorDetails, Error } from '../../../../common/runtime_types'; @@ -184,155 +184,6 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter { return monitorChartsData; } - /** - * Provides a count of the current monitors - * @param request Kibana request - * @param dateRangeStart timestamp bounds - * @param dateRangeEnd timestamp bounds - * @param filters filters defined by client - */ - public async getSnapshotCount( - request: any, - dateRangeStart: string, - dateRangeEnd: string, - filters?: string | null, - statusFilter?: string | null - ): Promise { - const query = parseFilterQuery(filters); - const additionalFilters = [{ exists: { field: 'summary.up' } }]; - if (query) { - additionalFilters.push(query); - } - const filter = getFilterClause(dateRangeStart, dateRangeEnd, additionalFilters); - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - query: { - bool: { - filter, - }, - }, - size: 0, - aggs: { - ids: { - composite: { - sources: [ - { - id: { - terms: { - field: 'monitor.id', - }, - }, - }, - { - location: { - terms: { - field: 'observer.geo.name', - missing_bucket: true, - }, - }, - }, - ], - size: QUERY.DEFAULT_AGGS_CAP, - }, - aggs: { - latest: { - top_hits: { - sort: [{ '@timestamp': { order: 'desc' } }], - _source: { - includes: ['summary.*', 'monitor.id', '@timestamp', 'observer.geo.name'], - }, - size: 1, - }, - }, - }, - }, - }, - }, - }; - - let searchAfter: any = null; - - const summaryByIdLocation: { - // ID - [key: string]: { - // Location - [key: string]: { up: number; down: number; timestamp: number }; - }; - } = {}; - - do { - if (searchAfter) { - set(params, 'body.aggs.ids.composite.after', searchAfter); - } - - const queryResult = await this.database.search(request, params); - const idBuckets = get(queryResult, 'aggregations.ids.buckets', []); - - idBuckets.forEach(bucket => { - // We only get the latest doc - const source: any = get(bucket, 'latest.hits.hits[0]._source'); - const { - summary: { up, down }, - monitor: { id }, - } = source; - const timestamp = get(source, '@timestamp', 0); - const location = get(source, 'observer.geo.name', ''); - - let idSummary = summaryByIdLocation[id]; - if (!idSummary) { - idSummary = {}; - summaryByIdLocation[id] = idSummary; - } - const locationSummary = idSummary[location]; - if (!locationSummary || locationSummary.timestamp < timestamp) { - idSummary[location] = { timestamp, up, down }; - } - }); - - searchAfter = get(queryResult, 'aggregations.ids.after_key'); - } while (searchAfter); - - let up: number = 0; - let mixed: number = 0; - let down: number = 0; - - for (const id in summaryByIdLocation) { - if (!summaryByIdLocation.hasOwnProperty(id)) { - continue; - } - const locationInfo = summaryByIdLocation[id]; - const { up: locationUp, down: locationDown } = reduce( - locationInfo, - (acc, value, key) => { - acc.up += value.up; - acc.down += value.down; - return acc; - }, - { up: 0, down: 0 } - ); - - if (locationDown === 0) { - up++; - } else if (locationUp > 0) { - mixed++; - } else { - down++; - } - } - - const result: any = { up, down, mixed, total: up + down + mixed }; - if (statusFilter) { - for (const status in result) { - if (status !== 'total' && status !== statusFilter) { - result[status] = 0; - } - } - } - - return result; - } - /** * Fetch options for the filter bar. * @param request Kibana request object diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts index cc702362a57a8..889f8a820b2f3 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts @@ -8,16 +8,18 @@ import { createIsValidRoute } from './auth'; import { createGetAllRoute } from './pings'; import { createGetIndexPatternRoute } from './index_pattern'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; +import { createGetSnapshotCount } from './snapshot'; import { UMRestApiRouteCreator } from './types'; import { createGetMonitorDetailsRoute } from './monitors'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; export const restApiRoutes: UMRestApiRouteCreator[] = [ - createIsValidRoute, createGetAllRoute, - createLogMonitorPageRoute, - createLogOverviewPageRoute, createGetIndexPatternRoute, createGetMonitorDetailsRoute, + createGetSnapshotCount, + createIsValidRoute, + createLogMonitorPageRoute, + createLogOverviewPageRoute, ]; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts new file mode 100644 index 0000000000000..ddca622005d63 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import { UMServerLibs } from '../../lib/lib'; +import { Snapshot } from '../../../common/runtime_types'; + +export const createGetSnapshotCount = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/snapshot/count', + options: { + validate: { + query: Joi.object({ + dateRangeStart: Joi.string().required(), + dateRangeEnd: Joi.string().required(), + filters: Joi.string(), + statusFilter: Joi.string(), + }), + }, + tags: ['access:uptime'], + }, + handler: async (request: any): Promise => { + const { dateRangeStart, dateRangeEnd, filters, statusFilter } = request.query; + return await libs.monitorStates.getSnapshotCount( + request, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter + ); + }, +}); diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts new file mode 100644 index 0000000000000..934b34ef1b397 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createGetSnapshotCount } from './get_snapshot_count'; diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json index 0ac6c67e23d2b..93d63bad66e30 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 7, - "mixed": 0, - "up": 93, - "total": 100 - } - } + "up": 93, + "down": 7, + "mixed": 0, + "total": 100 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json index 8639f0ec0feea..94c1ffbc74290 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_empty.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 13, - "mixed": 0, - "up": 0, - "total": 13 - } - } + "up": 0, + "down": 7, + "mixed": 0, + "total": 7 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json index 8639f0ec0feea..94c1ffbc74290 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_down.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 13, - "mixed": 0, - "up": 0, - "total": 13 - } - } + "up": 0, + "down": 7, + "mixed": 0, + "total": 7 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json index 065c3f90e932e..2d79880e7c0ee 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json +++ b/x-pack/test/api_integration/apis/uptime/graphql/fixtures/snapshot_filtered_by_up.json @@ -1,10 +1,6 @@ { - "snapshot": { - "counts": { - "down": 0, - "mixed": 0, - "up": 94, - "total": 94 - } - } + "up": 93, + "down": 0, + "mixed": 0, + "total": 93 } \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/graphql/index.js b/x-pack/test/api_integration/apis/uptime/graphql/index.js index f7fafa9419657..346032f87dc4d 100644 --- a/x-pack/test/api_integration/apis/uptime/graphql/index.js +++ b/x-pack/test/api_integration/apis/uptime/graphql/index.js @@ -17,7 +17,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./monitor_states')); loadTestFile(require.resolve('./monitor_status_bar')); loadTestFile(require.resolve('./ping_list')); - loadTestFile(require.resolve('./snapshot')); loadTestFile(require.resolve('./snapshot_histogram')); }); } diff --git a/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js b/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js deleted file mode 100644 index 004b87571eab4..0000000000000 --- a/x-pack/test/api_integration/apis/uptime/graphql/snapshot.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { snapshotQueryString } from '../../../../../legacy/plugins/uptime/public/queries'; -import { expectFixtureEql } from './helpers/expect_fixture_eql'; - -export default function ({ getService }) { - describe('snapshot query', () => { - before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat')); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat')); - - const supertest = getService('supertest'); - - it('will fetch a monitor snapshot summary', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - expectFixtureEql(data, 'snapshot'); - }); - - it('will fetch a monitor snapshot filtered by down status', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`, - statusFilter: 'down', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - expectFixtureEql(data, 'snapshot_filtered_by_down'); - }); - - it('will fetch a monitor snapshot filtered by up status', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-28T17:40:08.078Z', - dateRangeEnd: '2025-01-28T19:00:16.078Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"up","operator":"and"}}}]}}`, - statusFilter: 'up', - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - - expectFixtureEql(data, 'snapshot_filtered_by_up'); - }); - - it('returns null histogram data when no data present', async () => { - const getSnapshotQuery = { - operationName: 'Snapshot', - query: snapshotQueryString, - variables: { - dateRangeStart: '2019-01-25T04:30:54.740Z', - dateRangeEnd: '2025-01-28T04:50:54.740Z', - filters: `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`, - }, - }; - const { - body: { data }, - } = await supertest - .post('/api/uptime/graphql') - .set('kbn-xsrf', 'foo') - .send({ ...getSnapshotQuery }); - - - expectFixtureEql(data, 'snapshot_empty'); - }); - // TODO: test for host, port, etc. - }); -} diff --git a/x-pack/test/api_integration/apis/uptime/index.js b/x-pack/test/api_integration/apis/uptime/index.js index 6eb77fb584133..9175658783fb5 100644 --- a/x-pack/test/api_integration/apis/uptime/index.js +++ b/x-pack/test/api_integration/apis/uptime/index.js @@ -17,5 +17,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./get_all_pings')); loadTestFile(require.resolve('./graphql')); + loadTestFile(require.resolve('./rest')); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts new file mode 100644 index 0000000000000..b76d3f7c2e44a --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + describe('uptime REST endpoints', () => { + before('load heartbeat data', () => esArchiver.load('uptime/full_heartbeat')); + after('unload', () => esArchiver.unload('uptime/full_heartbeat')); + loadTestFile(require.resolve('./snapshot')); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts new file mode 100644 index 0000000000000..0175dc649b495 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/snapshot.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('snapshot count', () => { + let dateRangeStart = '2019-01-28T17:40:08.078Z'; + let dateRangeEnd = '2025-01-28T19:00:16.078Z'; + + it('will fetch the full set of snapshot counts', async () => { + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` + ); + expectFixtureEql(apiResponse.body, 'snapshot'); + }); + + it('will fetch a monitor snapshot filtered by down status', async () => { + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`; + const statusFilter = 'down'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down'); + }); + + it('will fetch a monitor snapshot filtered by up status', async () => { + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"up","operator":"and"}}}]}}`; + const statusFilter = 'up'; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up'); + }); + + it('returns a null snapshot when no data is present', async () => { + dateRangeStart = '2019-01-25T04:30:54.740Z'; + dateRangeEnd = '2025-01-28T04:50:54.740Z'; + const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`; + const apiResponse = await supertest.get( + `/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}` + ); + expectFixtureEql(apiResponse.body, 'snapshot_empty'); + }); + }); +} From 2d696eb7ba854f5513773a5ef282e01270254a25 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 27 Nov 2019 08:58:49 +0100 Subject: [PATCH 102/128] [APM] Handle APM UI config keys (#51668) `xpack.apm.ui.*` keys were not properly handled due to object path parsing. --- x-pack/plugins/apm/server/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index b66850ff569cb..082216a78ce5e 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -13,8 +13,11 @@ export const config = { schema: schema.object({ serviceMapEnabled: schema.boolean({ defaultValue: false }), autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), - 'ui.transactionGroupBucketSize': schema.number({ defaultValue: 100 }), - 'ui.maxTraceItems': schema.number({ defaultValue: 1000 }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + transactionGroupBucketSize: schema.number({ defaultValue: 100 }), + maxTraceItems: schema.number({ defaultValue: 1000 }), + }), }), }; @@ -30,8 +33,9 @@ export function mergeConfigs(apmOssConfig: APMOSSConfig, apmConfig: APMXPackConf 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices, 'apm_oss.indexPattern': apmOssConfig.indexPattern, 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled, - 'xpack.apm.ui.maxTraceItems': apmConfig['ui.maxTraceItems'], - 'xpack.apm.ui.transactionGroupBucketSize': apmConfig['ui.transactionGroupBucketSize'], + 'xpack.apm.ui.enabled': apmConfig.ui.enabled, + 'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems, + 'xpack.apm.ui.transactionGroupBucketSize': apmConfig.ui.transactionGroupBucketSize, 'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern, }; } From 517fcb98b85c09c824c8c8e7cdad79a85d4f1cc3 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Wed, 27 Nov 2019 09:35:08 +0100 Subject: [PATCH 103/128] [SIEM] Fix Timeline drag and drop behavior (#51558) --- package.json | 9 +- packages/kbn-i18n/package.json | 2 +- packages/kbn-ui-framework/package.json | 4 +- .../plugins/kbn_tp_run_pipeline/package.json | 4 +- .../kbn_tp_custom_visualizations/package.json | 2 +- .../kbn_tp_embeddable_explorer/package.json | 2 +- .../kbn_tp_sample_panel_action/package.json | 2 +- x-pack/legacy/plugins/siem/package.json | 4 +- .../drag_and_drop/drag_drop_context.tsx | 14 + .../drag_drop_context_wrapper.tsx | 19 +- .../drag_and_drop/draggable_wrapper.tsx | 4 + .../components/drag_and_drop/helpers.test.ts | 20 +- .../components/drag_and_drop/helpers.ts | 3 + .../error_toast_dispatcher/index.test.tsx | 2 +- .../authentications_table/index.test.tsx | 2 +- .../siem/public/components/page/index.tsx | 1 - .../page/network/kpi_network/index.test.tsx | 4 +- .../network/network_dns_table/index.test.tsx | 2 +- .../network/network_http_table/index.test.tsx | 2 +- .../index.test.tsx | 4 +- .../network_top_n_flow_table/index.test.tsx | 4 +- .../page/network/tls_table/index.test.tsx | 2 +- .../page/network/users_table/index.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 4 + .../public/components/timeline/body/index.tsx | 91 +++---- .../public/components/timeline/styles.tsx | 11 +- x-pack/package.json | 8 +- yarn.lock | 241 +++++++----------- 28 files changed, 238 insertions(+), 231 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx diff --git a/package.json b/package.json index 2c8d4ad4307b1..45a376a291359 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,10 @@ "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", "**/image-diff/gm/debug": "^2.6.9", - "**/deepmerge": "^4.2.2" + "**/deepmerge": "^4.2.2", + "**/react": "16.8.6", + "**/react-dom": "16.8.6", + "**/react-test-renderer": "16.8.6" }, "workspaces": { "packages": [ @@ -213,10 +216,10 @@ "pug": "^2.0.3", "querystring-browser": "1.0.4", "raw-loader": "3.1.0", - "react": "^16.8.0", + "react": "^16.8.6", "react-addons-shallow-compare": "15.6.2", "react-color": "^2.13.8", - "react-dom": "^16.8.0", + "react-dom": "^16.8.6", "react-grid-layout": "^0.16.2", "react-hooks-testing-library": "^0.5.0", "react-input-range": "^1.3.0", diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 591faff64711d..0146111941044 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -28,7 +28,7 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^2.1.0", "prop-types": "^15.6.2", - "react": "^16.8.0", + "react": "^16.8.6", "react-intl": "^2.8.0" } } diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 472c801721ecf..ee5424370fb06 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -19,7 +19,7 @@ "focus-trap-react": "^3.1.1", "lodash": "npm:@elastic/lodash@3.10.1-kibana3", "prop-types": "15.6.0", - "react": "^16.8.0", + "react": "^16.8.6", "react-ace": "^5.9.0", "react-color": "^2.13.8", "tabbable": "1.1.3", @@ -57,7 +57,7 @@ "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", - "react-dom": "^16.8.0", + "react-dom": "^16.8.6", "react-redux": "^5.0.6", "react-router": "^3.2.0", "react-router-redux": "^4.0.8", diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 769acc52e207b..97ad71eaddd7c 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -8,7 +8,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.0", - "react-dom": "^16.8.0" + "react": "^16.8.6", + "react-dom": "^16.8.6" } } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 41e1e6baca0ec..ca584b4b4e771 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -8,6 +8,6 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.0" + "react": "^16.8.6" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index a0b03e52640fc..71545fa582c66 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -9,7 +9,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.0" + "react": "^16.8.6" }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 952d06c4873d4..d5c97bb212ea0 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -9,7 +9,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.0" + "react": "^16.8.6" }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index 29c26c5f674e3..d239961ee75d7 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -12,11 +12,11 @@ "devDependencies": { "@types/lodash": "^4.14.110", "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^10.0.1" + "@types/react-beautiful-dnd": "^11.0.3" }, "dependencies": { "lodash": "^4.17.15", - "react-beautiful-dnd": "^10.0.1", + "react-beautiful-dnd": "^12.1.1", "react-markdown": "^4.0.6" } } diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx new file mode 100644 index 0000000000000..ee9533341a4f8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/40309 + +import { MovementMode, DraggableId } from 'react-beautiful-dnd'; + +export interface BeforeCapture { + draggableId: DraggableId; + mode: MovementMode; +} diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index c513f7a451240..a3528158a0317 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -6,10 +6,11 @@ import { defaultTo, noop } from 'lodash/fp'; import React, { useCallback } from 'react'; -import { DragDropContext, DropResult, DragStart } from 'react-beautiful-dnd'; +import { DropResult, DragDropContext } from 'react-beautiful-dnd'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; +import { BeforeCapture } from './drag_drop_context'; import { BrowserFields } from '../../containers/source'; import { dragAndDropModel, dragAndDropSelectors } from '../../store'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; @@ -20,6 +21,7 @@ import { addProviderToTimeline, fieldWasDroppedOnTimelineColumns, IS_DRAGGING_CLASS_NAME, + IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME, providerWasDroppedOnTimeline, providerWasDroppedOnTimelineButton, draggableIsField, @@ -75,11 +77,16 @@ export const DragDropContextWrapperComponent = React.memo( if (!draggableIsField(result)) { document.body.classList.remove(IS_DRAGGING_CLASS_NAME); } + + if (draggableIsField(result)) { + document.body.classList.remove(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } }, [browserFields, dataProviders] ); return ( - + // @ts-ignore + {children} ); @@ -107,7 +114,7 @@ const mapStateToProps = (state: State) => { export const DragDropContextWrapper = connect(mapStateToProps)(DragDropContextWrapperComponent); -const onDragStart = (initial: DragStart) => { +const onBeforeCapture = (before: BeforeCapture) => { const x = window.pageXOffset !== undefined ? window.pageXOffset @@ -120,9 +127,13 @@ const onDragStart = (initial: DragStart) => { window.onscroll = () => window.scrollTo(x, y); - if (!draggableIsField(initial)) { + if (!draggableIsField(before)) { document.body.classList.add(IS_DRAGGING_CLASS_NAME); } + + if (draggableIsField(before)) { + document.body.classList.add(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } }; const enableScrolling = () => (window.onscroll = () => noop); diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index 0f0e61e0206ec..c314785511201 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -34,6 +34,10 @@ export const useDraggablePortalContext = () => useContext(DraggablePortalContext const Wrapper = styled.div` display: inline-block; max-width: 100%; + + [data-rbd-placeholder-context-id] { + display: none !important; + } `; Wrapper.displayName = 'Wrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts index 8d3334b05bfaf..af4b9b280f3cd 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts @@ -116,7 +116,7 @@ describe('helpers', () => { test('it returns false when the draggable is NOT content', () => { expect( draggableIsContent({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { @@ -230,10 +230,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns false when the destination is null', () => { + test('it returns false when the destination is undefined', () => { expect( destinationIsTimelineProviders({ - destination: null, + destination: undefined, draggableId: getDraggableId('685260508808089'), reason: 'DROP', source: { @@ -286,10 +286,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns returns false when the destination is null', () => { + test('it returns returns false when the destination is undefined', () => { expect( destinationIsTimelineColumns({ - destination: null, + destination: undefined, draggableId: getDraggableFieldId({ contextId: 'test', fieldId: 'event.action' }), reason: 'DROP', source: { @@ -342,10 +342,10 @@ describe('helpers', () => { ).toEqual(true); }); - test('it returns false when the destination is null', () => { + test('it returns false when the destination is undefined', () => { expect( destinationIsTimelineButton({ - destination: null, + destination: undefined, draggableId: getDraggableId('685260508808089'), reason: 'DROP', source: { @@ -436,10 +436,10 @@ describe('helpers', () => { ).toEqual('timeline'); }); - test('it returns returns an empty string when the destination is null', () => { + test('it returns returns an empty string when the destination is undefined', () => { expect( getTimelineIdFromDestination({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { @@ -558,7 +558,7 @@ describe('helpers', () => { test('it returns false when the draggable is NOT content', () => { expect( providerWasDroppedOnTimeline({ - destination: null, + destination: undefined, draggableId: `${draggableIdPrefix}.timeline.timeline.dataProvider.685260508808089`, reason: 'DROP', source: { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts index 415970474db4c..ae3a8828491e3 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts @@ -224,3 +224,6 @@ export const DRAG_TYPE_FIELD = 'drag-type-field'; /** This class is added to the document body while dragging */ export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; + +/** This class is added to the document body while timeline field dragging */ +export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx index 6c4bc797d39f8..6233fcfe7c823 100644 --- a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx @@ -30,7 +30,7 @@ describe('Error Toast Dispatcher', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(ErrorToastDispatcherComponent)'))).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx index 7dd5eccb4a6c6..71e61e2425373 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx @@ -49,7 +49,7 @@ describe('Authentication Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(AuthenticationTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index d56012de88929..582ef2d01fb7e 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -19,7 +19,6 @@ import styled, { createGlobalStyle } from 'styled-components'; SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly and `EuiPopover`, `EuiToolTip` global styles */ - export const AppGlobalStyle = createGlobalStyle` div.app-wrapper { background-color: rgba(0,0,0,0); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx index eb6204044bdb7..964617c4c85b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx @@ -42,7 +42,7 @@ describe('KpiNetwork Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('KpiNetworkComponent'))).toMatchSnapshot(); }); test('it renders the default widget', () => { @@ -59,7 +59,7 @@ describe('KpiNetwork Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('KpiNetworkComponent'))).toMatchSnapshot(); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx index 98f55b29c8fc4..8bf338d17c47b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx @@ -51,7 +51,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkDnsTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx index 277e136d776fa..c92661a909a6e 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx @@ -51,7 +51,7 @@ describe('NetworkHttp Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkHttpTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx index d8a5da6036f95..ca7a3c0bb4387 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx @@ -57,7 +57,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopCountriesTableComponent)'))).toMatchSnapshot(); }); test('it renders the IP Details NetworkTopCountries table', () => { const wrapper = shallow( @@ -82,7 +82,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopCountriesTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx index df9e0f9f89645..884825422beb0 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx @@ -57,7 +57,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopNFlowTableComponent)'))).toMatchSnapshot(); }); test('it renders the default NetworkTopNFlow table on the IP Details page', () => { @@ -83,7 +83,7 @@ describe('NetworkTopNFlow Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(NetworkTopNFlowTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx index 612896c878ef9..8c397053380c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx @@ -47,7 +47,7 @@ describe('Tls Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(TlsTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx index 00a0a34a2b30b..d178164fd3fd7 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx @@ -55,7 +55,7 @@ describe('Users Table Component', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Connect(UsersTableComponent)'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 8bf7b1543b923..65818b697e0b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -483,8 +483,12 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index 47fbcec4aab23..07e37346ac968 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -19,7 +19,7 @@ import { OnUnPinEvent, OnUpdateColumns, } from '../events'; -import { EventsTable, TimelineBody } from '../styles'; +import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { ColumnHeader } from './column_headers/column_header'; import { Events } from './events'; @@ -86,50 +86,53 @@ export const Body = React.memo( ); return ( - - - + <> + + + - - - + + + + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx index 856259f11ced9..1c1c8fac75cdc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -6,7 +6,9 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { rgba } from 'polished'; -import styled from 'styled-components'; +import styled, { createGlobalStyle } from 'styled-components'; + +import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; /** * OFFSET PIXEL VALUES @@ -18,6 +20,13 @@ export const OFFSET_SCROLLBAR = 17; * TIMELINE BODY */ +// SIDE EFFECT: the following creates a global class selector +export const TimelineBodyGlobalStyle = createGlobalStyle` + body.${IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME} .siemTimeline__body { + overflow: hidden; + } +`; + export const TimelineBody = styled.div.attrs(({ className }) => ({ className: `siemTimeline__body ${className}`, }))<{ bodyHeight: number }>` diff --git a/x-pack/package.json b/x-pack/package.json index d97fd38676bde..c5114500c6f61 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -92,7 +92,7 @@ "@types/react-resize-detector": "^4.0.1", "@types/react-router-dom": "^4.3.1", "@types/react-sticky": "^6.0.3", - "@types/react-test-renderer": "^16.8.0", + "@types/react-test-renderer": "^16.8.3", "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^0.3.0", "@types/redux-actions": "^2.2.1", @@ -153,7 +153,7 @@ "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-hooks-testing-library": "^0.3.8", - "react-test-renderer": "^16.8.0", + "react-test-renderer": "^16.8.6", "react-testing-library": "^6.0.0", "sass-loader": "^7.3.1", "sass-resources-loader": "^2.0.1", @@ -293,11 +293,11 @@ "puid": "1.0.7", "puppeteer-core": "^1.19.0", "raw-loader": "3.1.0", - "react": "^16.8.0", + "react": "^16.8.6", "react-apollo": "^2.1.4", "react-beautiful-dnd": "^8.0.7", "react-datetime": "^2.14.0", - "react-dom": "^16.8.0", + "react-dom": "^16.8.6", "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", "react-markdown": "^3.4.1", diff --git a/yarn.lock b/yarn.lock index 1cf41a3ecd57c..e12a0eb46c6cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -982,6 +982,14 @@ core-js "^2.6.5" regenerator-runtime "^0.13.2" +"@babel/runtime-corejs2@^7.6.3": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.7.4.tgz#b9c2b1b5882762005785bc47740195a0ac780888" + integrity sha512-hKNcmHQbBSJFnZ82ewYtWDZ3fXkP/l1XcfRtm7c8gHPM/DMecJtFFBEp7KMLZTuHwwb7RfemHdsEnd7L916Z6A== + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.2" + "@babel/runtime@7.0.0-beta.54": version "7.0.0-beta.54" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf" @@ -3735,13 +3743,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-beautiful-dnd@^10.0.1": - version "10.1.2" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.2.tgz#74069f7b1d0cb67b7af99a2584b30e496e545d8b" - integrity sha512-76M5VRbhduUarM9wyMWQm3tLKCVMKTlhG0+W67dteg/HBE+kueIwuyLWzE0m5fmuilvrDXoM5NL890KLnHETZw== - dependencies: - "@types/react" "*" - "@types/react-beautiful-dnd@^10.1.0": version "10.1.1" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#7afae39a4247f30c13b8bbb726ccd1b8cda9d4a5" @@ -3749,6 +3750,13 @@ dependencies: "@types/react" "*" +"@types/react-beautiful-dnd@^11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz#51d9f37942dd18cc4aa10da98a5c883664e7ee46" + integrity sha512-7ZbT/7mNJu+uRrUGdTQ1hAINtqg909L4NHrXyspV42fvVgBgda6ysiBzoDUMENmQ/RlRJdpyrcp8Dtd/77bp9Q== + dependencies: + "@types/react" "*" + "@types/react-color@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0" @@ -3829,10 +3837,10 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@^16.8.0": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.8.1.tgz#96f3ce45a3a41c94eca532a99103dd3042c9d055" - integrity sha512-8gU69ELfJGxzVWVYj4MTtuHxz9nO+d175XeQ1XrXXxesUBsB4KK6OCfzVhEX6leZWWBDVtMJXp/rUjhClzL7gw== +"@types/react-test-renderer@^16.8.3": + version "16.9.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4" + integrity sha512-nCXQokZN1jp+QkoDNmDZwoWpKY8HDczqevIDO4Uv9/s9rbGPbSpy8Uaxa5ixHKkcm/Wt0Y9C3wCxZivh4Al+rQ== dependencies: "@types/react" "*" @@ -4533,7 +4541,7 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.0.0.tgz#c8ba6f0f1aac4b0a9e32d1f0af12be769528f36b" integrity sha512-7Bv1We7ZGuU79zZbb6rRqcpxo3OY+zrdtloZWoyD8fmGX+FeXRjE+iuGkZjSXLVovLzrsvMGMy0EkwA0E0umxg== -acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.2.1, acorn@^5.5.0: +acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.5.0: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== @@ -6366,11 +6374,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base62@^1.1.0: - version "1.2.8" - resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" - integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== - base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -8224,11 +8227,6 @@ commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0, comm resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -commander@^2.5.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^2.8.1: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -8258,21 +8256,6 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -commoner@^0.10.1: - version "0.10.8" - resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" - integrity sha1-NPw2cs0kOT6LtH5wyqApOBH08sU= - dependencies: - commander "^2.5.0" - detective "^4.3.1" - glob "^5.0.15" - graceful-fs "^4.1.2" - iconv-lite "^0.4.5" - mkdirp "^0.5.0" - private "^0.1.6" - q "^1.1.2" - recast "^0.11.17" - compare-versions@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" @@ -8976,6 +8959,13 @@ css-box-model@^1.1.1: dependencies: tiny-invariant "^1.0.3" +css-box-model@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.0.tgz#3a26377b4162b3200d2ede4b064ec5b6a75186d0" + integrity sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -9805,7 +9795,7 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defined@^1.0.0, defined@~1.0.0: +defined@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= @@ -10054,14 +10044,6 @@ detective-typescript@^5.1.1: node-source-walk "^4.2.0" typescript "^3.4.5" -detective@^4.3.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" - integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig== - dependencies: - acorn "^5.2.1" - defined "^1.0.0" - dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -10740,14 +10722,6 @@ env-variable@0.0.x: resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88" integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA== -envify@^3.0.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/envify/-/envify-3.4.1.tgz#d7122329e8df1688ba771b12501917c9ce5cbce8" - integrity sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg= - dependencies: - jstransform "^11.0.3" - through "~2.3.4" - enzyme-adapter-react-16@^1.15.1: version "1.15.1" resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5" @@ -11439,11 +11413,6 @@ espree@^6.1.1: acorn-jsx "^5.0.2" eslint-visitor-keys "^1.1.0" -esprima-fb@^15001.1.0-dev-harmony-fb: - version "15001.1.0-dev-harmony-fb" - resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901" - integrity sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE= - esprima@2.7.x, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -12118,17 +12087,6 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.6.1.tgz#9636b7705f5ba9684d44b72f78321254afc860f7" - integrity sha1-lja3cF9bqWhNRLcveDISVK/IYPc= - dependencies: - core-js "^1.0.0" - loose-envify "^1.0.0" - promise "^7.0.3" - ua-parser-js "^0.7.9" - whatwg-fetch "^0.9.0" - fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.16: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" @@ -14999,7 +14957,7 @@ icalendar@0.7.1: resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae" integrity sha1-0NNIZ5X48cXPT4yvrAgbS056Mq4= -iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -17246,17 +17204,6 @@ jssha@^2.1.0: resolved "https://registry.yarnpkg.com/jssha/-/jssha-2.3.1.tgz#147b2125369035ca4b2f7d210dc539f009b3de9a" integrity sha1-FHshJTaQNcpLL30hDcU58Amz3po= -jstransform@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223" - integrity sha1-CaeJk+CuTU70SH9hVakfYZDLQiM= - dependencies: - base62 "^1.1.0" - commoner "^0.10.1" - esprima-fb "^15001.1.0-dev-harmony-fb" - object-assign "^2.0.0" - source-map "^0.4.2" - jstransformer-ejs@^0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/jstransformer-ejs/-/jstransformer-ejs-0.0.3.tgz#04d9201469274fcf260f1e7efd732d487fa234b6" @@ -18844,6 +18791,11 @@ memoize-one@^5.0.1: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.2.tgz#6aba5276856d72fb44ead3efab86432f94ba203d" integrity sha512-o7lldN4fs/axqctc03NF+PMhd2veRrWeJ2n2GjEzUPBD4F9rmNg4A+bQCACIzwjHJEXuYv4aFFMaH35KZfHUrw== +memoize-one@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + memoizee@0.4.X: version "0.4.14" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" @@ -21954,7 +21906,7 @@ promise.prototype.finally@^3.1.0: es-abstract "^1.9.0" function-bind "^1.1.1" -promise@^7.0.1, promise@^7.0.3, promise@^7.1.1: +promise@^7.0.1, promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== @@ -22405,6 +22357,11 @@ raf-schd@^4.0.0: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.0.tgz#9855756c5045ff4ed4516e14a47719387c3c907b" integrity sha512-m7zq0JkIrECzw9mO5Zcq6jN4KayE34yoIS9hJoiZNXyOAT06PPA8PrR+WtJIeFW09YjUfNkMMN9lrmAt6BURCA== +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== + raf@^3.1.0, raf@^3.3.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" @@ -22600,7 +22557,7 @@ react-apollo@^2.1.4: lodash "^4.17.10" prop-types "^15.6.0" -react-beautiful-dnd@^10.0.1, react-beautiful-dnd@^10.1.0: +react-beautiful-dnd@^10.1.0: version "10.1.1" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#d753088d77d7632e77cf8a8935fafcffa38f574b" integrity sha512-TdE06Shfp56wm28EzjgC56EEMgGI5PDHejJ2bxuAZvZr8CVsbksklsJC06Hxf0MSL7FHbflL/RpkJck9isuxHg== @@ -22614,6 +22571,19 @@ react-beautiful-dnd@^10.0.1, react-beautiful-dnd@^10.1.0: redux "^4.0.1" tiny-invariant "^1.0.4" +react-beautiful-dnd@^12.1.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#810f9b9d94f667b15b253793e853d016a0f3f07c" + integrity sha512-w/mpIXMEXowc53PCEnMoFyAEYFgxMfygMK5msLo5ifJ2/CiSACLov9A79EomnPF7zno3N207QGXsraBxAJnyrw== + dependencies: + "@babel/runtime-corejs2" "^7.6.3" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-beautiful-dnd@^8.0.7: version "8.0.7" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-8.0.7.tgz#2cc7ba62bffe08d3dad862fd8f48204440901b43" @@ -22738,17 +22708,7 @@ react-docgen@^4.1.0: node-dir "^0.1.10" recast "^0.17.3" -react-dom@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.2.tgz#7c8a69545dd554d45d66442230ba04a6a0a3c3d3" - integrity sha512-cPGfgFfwi+VCZjk73buu14pYkYBR1b/SRMSYqkLDdhSEHnSwcuYTPu6/Bh6ZphJFIk80XLvbSe2azfcRzNF+Xg== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.2" - -react-dom@^16.8.3, react-dom@^16.8.5: +react-dom@16.8.6, react-dom@^16.8.3, react-dom@^16.8.5, react-dom@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== @@ -22911,7 +22871,7 @@ react-is@^16.10.2, react-is@^16.9.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2, react-is@^16.8.6: +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== @@ -23088,6 +23048,18 @@ react-redux@^5.1.1: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-redux@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" + integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-resizable@1.x: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" @@ -23236,7 +23208,7 @@ react-syntax-highlighter@^8.0.1: prismjs "^1.8.4" refractor "^2.4.1" -react-test-renderer@^16.0.0-0: +react-test-renderer@16.8.6, react-test-renderer@^16.0.0-0, react-test-renderer@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1" integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw== @@ -23246,16 +23218,6 @@ react-test-renderer@^16.0.0-0: react-is "^16.8.6" scheduler "^0.13.6" -react-test-renderer@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.2.tgz#3ce0bf12aa211116612fda01a886d6163c9c459b" - integrity sha512-gsd4NoOaYrZD2R8zi+CBV9wTGMsGhE2bRe4wvenGy0WcLJgdPscRZDDz+kmLjY+/5XpYC8yRR/v4CScgYfGyoQ== - dependencies: - object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.2" - scheduler "^0.13.2" - react-testing-library@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.0.tgz#81edfcfae8a795525f48685be9bf561df45bb35d" @@ -23343,25 +23305,7 @@ react-visibility-sensor@^5.1.1: dependencies: prop-types "^15.7.2" -react@^0.14.0: - version "0.14.9" - resolved "https://registry.yarnpkg.com/react/-/react-0.14.9.tgz#9110a6497c49d44ba1c0edd317aec29c2e0d91d1" - integrity sha1-kRCmSXxJ1EuhwO3TF67CnC4NkdE= - dependencies: - envify "^3.0.0" - fbjs "^0.6.1" - -react@^16.8.0: - version "16.8.2" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.2.tgz#83064596feaa98d9c2857c4deae1848b542c9c0c" - integrity sha512-aB2ctx9uQ9vo09HVknqv3DGRpI7OIGJhCx3Bt0QqoRluEjHSaObJl+nG12GDdYH6sTgE7YiPJ6ZUyMx9kICdXw== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.13.2" - -react@^16.8.3, react@^16.8.5: +react@16.8.6, react@^0.14.0, react@^16.8.3, react@^16.8.5, react@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== @@ -23594,16 +23538,6 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" -recast@^0.11.17, recast@~0.11.12: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - recast@^0.14.7: version "0.14.7" resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" @@ -23624,6 +23558,16 @@ recast@^0.17.3: private "^0.1.8" source-map "~0.6.1" +recast@~0.11.12: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -23749,6 +23693,14 @@ redux@^4.0.1: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -24809,7 +24761,7 @@ saxes@^3.1.3: dependencies: xmlchars "^2.1.1" -scheduler@^0.13.2, scheduler@^0.13.3, scheduler@^0.13.6: +scheduler@^0.13.3, scheduler@^0.13.6: version "0.13.6" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== @@ -26987,6 +26939,11 @@ tiny-invariant@^1.0.2, tiny-invariant@^1.0.3, tiny-invariant@^1.0.4: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g== +tiny-invariant@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" + integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== + tiny-lr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab" @@ -28407,6 +28364,11 @@ url@0.11.0, url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== + use@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" @@ -29570,11 +29532,6 @@ whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== -whatwg-fetch@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0" - integrity sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA= - whatwg-mimetype@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171" From 93654d5ff977a2f0f036240d97dc32887231174e Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 27 Nov 2019 09:49:54 +0100 Subject: [PATCH 104/128] [ML] Fix anomaly detection test suite (#51712) This PR re-enables the anomaly detection test suite and disables Firefox test execution for now. It also increases stability for `clickEditDetector` and removes unneeded retries. --- .../anomaly_detection/index.ts | 5 +++-- .../machine_learning/job_management.ts | 6 +----- .../machine_learning/job_wizard_advanced.ts | 20 ++++++++----------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index 2b76bce544f6d..d5d617587fc3b 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -6,8 +6,9 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { - // FLAKY: https://github.com/elastic/kibana/issues/51669 - describe.skip('anomaly detection', function() { + describe('anomaly detection', function() { + this.tags(['skipFirefox']); + loadTestFile(require.resolve('./single_metric_job')); loadTestFile(require.resolve('./multi_metric_job')); loadTestFile(require.resolve('./population_job')); diff --git a/x-pack/test/functional/services/machine_learning/job_management.ts b/x-pack/test/functional/services/machine_learning/job_management.ts index ddab5fd68f13c..5ffb235a828d6 100644 --- a/x-pack/test/functional/services/machine_learning/job_management.ts +++ b/x-pack/test/functional/services/machine_learning/job_management.ts @@ -15,7 +15,6 @@ export function MachineLearningJobManagementProvider( mlApi: ProvidedType ) { const testSubjects = getService('testSubjects'); - const retry = getService('retry'); return { async navigateToNewJobSourceSelection() { @@ -36,10 +35,7 @@ export function MachineLearningJobManagementProvider( }, async assertStartDatafeedModalExists() { - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlStartDatafeedModal'); - }); + await testSubjects.existOrFail('mlStartDatafeedModal', { timeout: 5000 }); }, async confirmStartDatafeedModal() { diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts index 71b76a6885592..3e7dacb23d61b 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts @@ -146,10 +146,7 @@ export function MachineLearningJobWizardAdvancedProvider({ }, async assertCreateDetectorModalExists() { - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlCreateDetectorModal'); - }); + await testSubjects.existOrFail('mlCreateDetectorModal', { timeout: 5000 }); }, async assertDetectorFunctionInputExists() { @@ -298,18 +295,17 @@ export function MachineLearningJobWizardAdvancedProvider({ }, async clickEditDetector(detectorIndex: number) { - await testSubjects.click( - `mlAdvancedDetector ${detectorIndex} > mlAdvancedDetectorEditButton` - ); - await this.assertCreateDetectorModalExists(); + await retry.tryForTime(20 * 1000, async () => { + await testSubjects.click( + `mlAdvancedDetector ${detectorIndex} > mlAdvancedDetectorEditButton` + ); + await this.assertCreateDetectorModalExists(); + }); }, async createJob() { await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob'); - // this retry can be removed as soon as #48734 is merged - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlStartDatafeedModal'); - }); + await testSubjects.existOrFail('mlStartDatafeedModal', { timeout: 10 * 1000 }); }, }; } From 85ede04f854ce1faba57df957ed32a07bbc2263a Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Wed, 27 Nov 2019 10:39:46 +0100 Subject: [PATCH 105/128] fixes browser field tests (#51738) --- .../integration/lib/fields_browser/helpers.ts | 2 +- .../fields_browser/fields_browser.spec.ts | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts index bc6d037432771..e3495b6a78127 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/fields_browser/helpers.ts @@ -19,7 +19,7 @@ import { TIMELINE_DATA_PROVIDERS } from '../timeline/selectors'; /** Opens the timeline's Field Browser */ export const openTimelineFieldsBrowser = () => { - cy.get(TIMELINE_FIELDS_BUTTON).click(); + cy.get(TIMELINE_FIELDS_BUTTON).click({ force: true }); cy.get(FIELDS_BROWSER_CONTAINER).should('exist'); }; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts index baf6b7cd2027d..2d613ab09f1c1 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts @@ -34,7 +34,7 @@ const defaultHeaders = [ { id: 'user.name' }, ]; -describe.skip('Fields Browser', () => { +describe('Fields Browser', () => { beforeEach(() => { loginAndWaitForPage(HOSTS_PAGE); }); @@ -104,9 +104,9 @@ describe.skip('Fields Browser', () => { openTimelineFieldsBrowser(); - cy.get( - `[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]` - ).uncheck(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).uncheck({ + force: true, + }); clickOutsideFieldsBrowser(); @@ -185,7 +185,9 @@ describe.skip('Fields Browser', () => { 'not.exist' ); - cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).check(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).check({ + force: true, + }); clickOutsideFieldsBrowser(); @@ -194,7 +196,7 @@ describe.skip('Fields Browser', () => { ); }); - it('adds a field to the timeline when the user drags and drops a field', () => { + it.skip('adds a field to the timeline when the user drags and drops a field', () => { const filterInput = 'host.geo.c'; const toggleField = 'host.geo.city_name'; @@ -235,7 +237,9 @@ describe.skip('Fields Browser', () => { 'not.exist' ); - cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).check(); + cy.get(`[data-test-subj="timeline"] [data-test-subj="field-${toggleField}-checkbox"]`).check({ + force: true, + }); clickOutsideFieldsBrowser(); @@ -245,7 +249,7 @@ describe.skip('Fields Browser', () => { openTimelineFieldsBrowser(); - cy.get('[data-test-subj="timeline"] [data-test-subj="reset-fields"]').click(); + cy.get('[data-test-subj="timeline"] [data-test-subj="reset-fields"]').click({ force: true }); cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${toggleField}"]`).should( 'not.exist' From 04b91e786e66c2ca94ae1c017650f11e9f7f8edb Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Wed, 27 Nov 2019 10:40:20 +0100 Subject: [PATCH 106/128] fixes url state tests (#51746) --- .../cypress/integration/lib/url_state/index.ts | 14 +++++++------- .../smoke_tests/url_state/url_state.spec.ts | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts index 5c12bd528030e..ef1892b3d382c 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts @@ -16,20 +16,20 @@ export const ABSOLUTE_DATE_RANGE = { endTimeFormat: '2019-08-01T20:33:29.186Z', endTimeTimeline: '1564779809186', endTimeTimelineFormat: '2019-08-02T21:03:29.186Z', - endTimeTimelineTyped: '2019-08-02 21:03:29.186', - endTimeTyped: '2019-08-01 14:33:29.186', + endTimeTimelineTyped: 'Aug 02, 2019 @ 21:03:29.186', + endTimeTyped: 'Aug 01, 2019 @ 14:33:29.186', newEndTime: '1564693409186', newEndTimeFormat: '2019-08-01T21:03:29.186Z', - newEndTimeTyped: '2019-08-01 15:03:29.186', + newEndTimeTyped: 'Aug 01, 2019 @ 15:03:29.186', newStartTime: '1564691609186', newStartTimeFormat: '2019-08-01T20:33:29.186Z', - newStartTimeTyped: '2019-08-01 14:33:29.186', + newStartTimeTyped: 'Aug 01, 2019 @ 14:33:29.186', startTime: '1564689809186', startTimeFormat: '2019-08-01T20:03:29.186Z', startTimeTimeline: '1564776209186', startTimeTimelineFormat: '2019-08-02T20:03:29.186Z', - startTimeTimelineTyped: '2019-08-02 14:03:29.186', - startTimeTyped: '2019-08-01 14:03:29.186', + startTimeTimelineTyped: 'Aug 02, 2019 @ 14:03:29.186', + startTimeTyped: 'Aug 01, 2019 @ 14:03:29.186', url: '/app/siem#/network/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', @@ -52,7 +52,7 @@ export const DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE = '[data-test-subj="timeline-properties"] [data-test-subj="superDatePickerendDatePopoverButton"]'; export const DATE_PICKER_ABSOLUTE_TAB = '[data-test-subj="superDatePickerAbsoluteTab"]'; export const DATE_PICKER_APPLY_BUTTON = - '[data-test-subj="globalDatePicker"] button[data-test-subj="superDatePickerApplyTimeButton"]'; + '[data-test-subj="globalDatePicker"] button[data-test-subj="querySubmitButton"]'; export const DATE_PICKER_APPLY_BUTTON_TIMELINE = '[data-test-subj="timeline-properties"] button[data-test-subj="superDatePickerApplyTimeButton"]'; export const DATE_PICKER_ABSOLUTE_INPUT = '[data-test-subj="superDatePickerAbsoluteDateInput"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts index 4ba8b8c44f366..b1867a437f7f4 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts @@ -51,7 +51,7 @@ describe('url state', () => { ); }); - it.skip('sets the url state when start and end date are set', () => { + it('sets the url state when start and end date are set', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.url); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).click({ force: true }); @@ -64,7 +64,7 @@ describe('url state', () => { `{selectall}{backspace}${ABSOLUTE_DATE_RANGE.newStartTimeTyped}` ); - cy.get(DATE_PICKER_APPLY_BUTTON).click({ force: true }); + cy.get(DATE_PICKER_APPLY_BUTTON, { timeout: 5000 }).click(); cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).click({ force: true }); @@ -76,7 +76,7 @@ describe('url state', () => { `{selectall}{backspace}${ABSOLUTE_DATE_RANGE.newEndTimeTyped}` ); - cy.get(DATE_PICKER_APPLY_BUTTON).click({ force: true }); + cy.get(DATE_PICKER_APPLY_BUTTON, { timeout: 5000 }).click(); cy.url().should( 'include', @@ -127,7 +127,7 @@ describe('url state', () => { ); }); - it.skip('sets the url state when timeline/global date pickers are unlinked and timeline start and end date are set', () => { + it('sets the url state when timeline/global date pickers are unlinked and timeline start and end date are set', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlUnlinked); toggleTimelineVisibility(); @@ -165,17 +165,17 @@ describe('url state', () => { ); }); - it.skip('sets kql on network page', () => { + it('sets kql on network page', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlKqlNetworkNetwork); cy.get(KQL_INPUT, { timeout: 5000 }).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); }); - it.skip('sets kql on hosts page', () => { + it('sets kql on hosts page', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts); cy.get(KQL_INPUT, { timeout: 5000 }).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); }); - it.skip('sets the url state when kql is set', () => { + it('sets the url state when kql is set', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.url); cy.get(KQL_INPUT, { timeout: 5000 }).type('source.ip: "10.142.0.9" {enter}'); cy.url().should('include', `query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')`); @@ -241,7 +241,7 @@ describe('url state', () => { ); }); - it.skip('Do not clears kql when navigating to a new page', () => { + it('Do not clears kql when navigating to a new page', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts); cy.get(NAVIGATION_NETWORK).click({ force: true }); cy.get(KQL_INPUT, { timeout: 5000 }).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); From 0fd63ee5ce4bca19e2b2d13c5fa9390cfcfeefbc Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 27 Nov 2019 10:55:28 +0100 Subject: [PATCH 107/128] Expressions service fixes: better error and loading states handling (#51183) 1. This pr fixes regression in v7.6 - #51153. Visualisation which are using ExpressionLoader directly are swallowing render errors in 7.6, because generic error handling is happening only on expression_renderer.tsx level (react component wrapper around renderer). Now react agnostic render code render.ts, loader.ts: has it's own default error handler which is toast from notification service. It also allows to pass custom error handler instead of default one React expression renderer expression_renderer.tsx: if consumer wants to render error as react component inline, then the component uses custom error handler from render.ts to wire up react component. 2. This pr fixes issue of loader.ts where initial loading$ was not emitted Had to change loadingSubject from Subject to BehaviorSubject to remember the last value 3. This pr fixes dependencies in effects of expression_renderer.tsx --- .../expressions/public/execute.test.ts | 7 + .../public/expression_renderer.test.tsx | 83 +++++----- .../public/expression_renderer.tsx | 144 ++++++++++-------- src/plugins/expressions/public/index.ts | 2 +- src/plugins/expressions/public/loader.test.ts | 23 +-- src/plugins/expressions/public/loader.ts | 33 ++-- src/plugins/expressions/public/plugin.ts | 9 +- src/plugins/expressions/public/render.test.ts | 134 ++++++++++------ src/plugins/expressions/public/render.ts | 89 +++++------ .../public/render_error_handler.ts | 36 +++++ src/plugins/expressions/public/services.ts | 4 + src/plugins/expressions/public/types/index.ts | 14 ++ src/plugins/kibana_react/public/index.ts | 2 +- src/plugins/kibana_react/public/util/index.ts | 1 + .../util/use_shallow_compare_effect.test.ts | 86 +++++++++++ .../public/util/use_shallow_compare_effect.ts | 80 ++++++++++ .../public/np_ready/app/components/main.tsx | 22 +-- .../public/np_ready/types.ts | 4 +- .../test_suites/run_pipeline/helpers.ts | 6 +- .../embeddable/expression_wrapper.tsx | 1 + 20 files changed, 547 insertions(+), 233 deletions(-) create mode 100644 src/plugins/expressions/public/render_error_handler.ts create mode 100644 src/plugins/kibana_react/public/util/use_shallow_compare_effect.test.ts create mode 100644 src/plugins/kibana_react/public/util/use_shallow_compare_effect.ts diff --git a/src/plugins/expressions/public/execute.test.ts b/src/plugins/expressions/public/execute.test.ts index b60c4aed89fcf..6700ec38df940 100644 --- a/src/plugins/expressions/public/execute.test.ts +++ b/src/plugins/expressions/public/execute.test.ts @@ -29,6 +29,13 @@ jest.mock('./services', () => ({ }, }; }, + getNotifications: jest.fn(() => { + return { + toasts: { + addError: jest.fn(() => {}), + }, + }; + }), })); describe('execute helper function', () => { diff --git a/src/plugins/expressions/public/expression_renderer.test.tsx b/src/plugins/expressions/public/expression_renderer.test.tsx index 26db8753e6403..217618bc3a177 100644 --- a/src/plugins/expressions/public/expression_renderer.test.tsx +++ b/src/plugins/expressions/public/expression_renderer.test.tsx @@ -18,12 +18,14 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { Subject } from 'rxjs'; import { share } from 'rxjs/operators'; import { ExpressionRendererImplementation } from './expression_renderer'; import { ExpressionLoader } from './loader'; import { mount } from 'enzyme'; import { EuiProgress } from '@elastic/eui'; +import { RenderErrorHandlerFnType } from './types'; jest.mock('./loader', () => { return { @@ -54,60 +56,38 @@ describe('ExpressionRenderer', () => { const instance = mount(); - loadingSubject.next(); + act(() => { + loadingSubject.next(); + }); + instance.update(); expect(instance.find(EuiProgress)).toHaveLength(1); - renderSubject.next(1); + act(() => { + renderSubject.next(1); + }); instance.update(); expect(instance.find(EuiProgress)).toHaveLength(0); instance.setProps({ expression: 'something new' }); - loadingSubject.next(); + act(() => { + loadingSubject.next(); + }); instance.update(); expect(instance.find(EuiProgress)).toHaveLength(1); - - renderSubject.next(1); - instance.update(); - - expect(instance.find(EuiProgress)).toHaveLength(0); - }); - - it('should display an error message when the expression fails', () => { - const dataSubject = new Subject(); - const data$ = dataSubject.asObservable().pipe(share()); - const renderSubject = new Subject(); - const render$ = renderSubject.asObservable().pipe(share()); - const loadingSubject = new Subject(); - const loading$ = loadingSubject.asObservable().pipe(share()); - - (ExpressionLoader as jest.Mock).mockImplementation(() => { - return { - render$, - data$, - loading$, - update: jest.fn(), - }; - }); - - const instance = mount(); - - dataSubject.next('good data'); - renderSubject.next({ - type: 'error', - error: { message: 'render error' }, + act(() => { + renderSubject.next(1); }); instance.update(); expect(instance.find(EuiProgress)).toHaveLength(0); - expect(instance.find('[data-test-subj="expression-renderer-error"]')).toHaveLength(1); }); - it('should display a custom error message if the user provides one', () => { + it('should display a custom error message if the user provides one and then remove it after successful render', () => { const dataSubject = new Subject(); const data$ = dataSubject.asObservable().pipe(share()); const renderSubject = new Subject(); @@ -115,7 +95,10 @@ describe('ExpressionRenderer', () => { const loadingSubject = new Subject(); const loading$ = loadingSubject.asObservable().pipe(share()); - (ExpressionLoader as jest.Mock).mockImplementation(() => { + let onRenderError: RenderErrorHandlerFnType; + (ExpressionLoader as jest.Mock).mockImplementation((...args) => { + const params = args[2]; + onRenderError = params.onRenderError; return { render$, data$, @@ -124,18 +107,32 @@ describe('ExpressionRenderer', () => { }; }); - const renderErrorFn = jest.fn().mockReturnValue(null); - const instance = mount( - +
    {message}
    } + /> ); - renderSubject.next({ - type: 'error', - error: { message: 'render error' }, + act(() => { + onRenderError!(instance.getDOMNode(), new Error('render error'), { + done: () => { + renderSubject.next(1); + }, + } as any); }); + instance.update(); + expect(instance.find(EuiProgress)).toHaveLength(0); + expect(instance.find('[data-test-subj="custom-error"]')).toHaveLength(1); + expect(instance.find('[data-test-subj="custom-error"]').contains('render error')).toBeTruthy(); - expect(renderErrorFn).toHaveBeenCalledWith('render error'); + act(() => { + loadingSubject.next(); + renderSubject.next(2); + }); + instance.update(); + expect(instance.find(EuiProgress)).toHaveLength(0); + expect(instance.find('[data-test-subj="custom-error"]')).toHaveLength(0); }); }); diff --git a/src/plugins/expressions/public/expression_renderer.tsx b/src/plugins/expressions/public/expression_renderer.tsx index b4f0a509c81b6..3989f4ed7d698 100644 --- a/src/plugins/expressions/public/expression_renderer.tsx +++ b/src/plugins/expressions/public/expression_renderer.tsx @@ -17,12 +17,15 @@ * under the License. */ -import { useRef, useEffect, useState } from 'react'; +import { useRef, useEffect, useState, useLayoutEffect } from 'react'; import React from 'react'; import classNames from 'classnames'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { EuiLoadingChart, EuiProgress } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { IExpressionLoaderParams } from './types'; +import { useShallowCompareEffect } from '../../kibana_react/public'; +import { IExpressionLoaderParams, IInterpreterRenderHandlers, RenderError } from './types'; import { ExpressionAST } from '../common/types'; import { ExpressionLoader } from './loader'; @@ -39,7 +42,7 @@ export interface ExpressionRendererProps extends IExpressionLoaderParams { interface State { isEmpty: boolean; isLoading: boolean; - error: null | { message: string }; + error: null | RenderError; } export type ExpressionRenderer = React.FC; @@ -53,73 +56,94 @@ const defaultState: State = { export const ExpressionRendererImplementation = ({ className, dataAttrs, - expression, - renderError, padding, - ...options + renderError, + expression, + ...expressionLoaderOptions }: ExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); - const handlerRef: React.MutableRefObject = useRef(null); const [state, setState] = useState({ ...defaultState }); + const hasCustomRenderErrorHandler = !!renderError; + const expressionLoaderRef: React.MutableRefObject = useRef(null); + // flag to skip next render$ notification, + // because of just handled error + const hasHandledErrorRef = useRef(false); - // Re-fetch data automatically when the inputs change - /* eslint-disable react-hooks/exhaustive-deps */ - useEffect(() => { - if (handlerRef.current) { - handlerRef.current.update(expression, options); - } - }, [ - expression, - options.searchContext, - options.context, - options.variables, - options.disableCaching, - ]); - /* eslint-enable react-hooks/exhaustive-deps */ + // will call done() in LayoutEffect when done with rendering custom error state + const errorRenderHandlerRef: React.MutableRefObject = useRef( + null + ); - // Initialize the loader only once + /* eslint-disable react-hooks/exhaustive-deps */ + // OK to ignore react-hooks/exhaustive-deps because options update is handled by calling .update() useEffect(() => { - if (mountpoint.current && !handlerRef.current) { - handlerRef.current = new ExpressionLoader(mountpoint.current, expression, options); + const subs: Subscription[] = []; + expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, expression, { + ...expressionLoaderOptions, + // react component wrapper provides different + // error handling api which is easier to work with from react + // if custom renderError is not provided then we fallback to default error handling from ExpressionLoader + onRenderError: hasCustomRenderErrorHandler + ? (domNode, error, handlers) => { + errorRenderHandlerRef.current = handlers; + setState(() => ({ + ...defaultState, + isEmpty: false, + error, + })); - handlerRef.current.loading$.subscribe(() => { - if (!handlerRef.current) { - return; - } + if (expressionLoaderOptions.onRenderError) { + expressionLoaderOptions.onRenderError(domNode, error, handlers); + } + } + : expressionLoaderOptions.onRenderError, + }); + subs.push( + expressionLoaderRef.current.loading$.subscribe(() => { + hasHandledErrorRef.current = false; setState(prevState => ({ ...prevState, isLoading: true })); - }); - handlerRef.current.render$.subscribe(item => { - if (!handlerRef.current) { - return; - } - if (typeof item !== 'number') { + }), + expressionLoaderRef.current.render$ + .pipe(filter(() => !hasHandledErrorRef.current)) + .subscribe(item => { setState(() => ({ ...defaultState, isEmpty: false, - error: item.error, })); - } else { - setState(() => ({ - ...defaultState, - isEmpty: false, - })); - } - }); - } - /* eslint-disable */ - // TODO: Replace mountpoint.current by something else. - }, [mountpoint.current]); - /* eslint-enable */ + }) + ); - useEffect(() => { - // We only want a clean up to run when the entire component is unloaded, not on every render - return function cleanup() { - if (handlerRef.current) { - handlerRef.current.destroy(); - handlerRef.current = null; + return () => { + subs.forEach(s => s.unsubscribe()); + if (expressionLoaderRef.current) { + expressionLoaderRef.current.destroy(); + expressionLoaderRef.current = null; } + + errorRenderHandlerRef.current = null; }; - }, []); + }, [hasCustomRenderErrorHandler]); + + // Re-fetch data automatically when the inputs change + useShallowCompareEffect( + () => { + if (expressionLoaderRef.current) { + expressionLoaderRef.current.update(expression, expressionLoaderOptions); + } + }, + // when expression is changed by reference and when any other loaderOption is changed by reference + [{ expression, ...expressionLoaderOptions }] + ); + + /* eslint-enable react-hooks/exhaustive-deps */ + // call expression loader's done() handler when finished rendering custom error state + useLayoutEffect(() => { + if (state.error && errorRenderHandlerRef.current) { + hasHandledErrorRef.current = true; + errorRenderHandlerRef.current.done(); + errorRenderHandlerRef.current = null; + } + }, [state.error]); const classes = classNames('expExpressionRenderer', { 'expExpressionRenderer-isEmpty': state.isEmpty, @@ -135,15 +159,9 @@ export const ExpressionRendererImplementation = ({ return (
    - {state.isEmpty ? : null} - {state.isLoading ? : null} - {!state.isLoading && state.error ? ( - renderError ? ( - renderError(state.error.message) - ) : ( -
    {state.error.message}
    - ) - ) : null} + {state.isEmpty && } + {state.isLoading && } + {!state.isLoading && state.error && renderError && renderError(state.error.message)}
    { getRenderersRegistry: () => ({ get: (id: string) => renderers[id], }), + getNotifications: jest.fn(() => { + return { + toasts: { + addError: jest.fn(() => {}), + }, + }; + }), }; }); @@ -97,20 +104,14 @@ describe('ExpressionLoader', () => { expect(response).toEqual({ type: 'render', as: 'test' }); }); - it('emits on loading$ when starting to load', async () => { + it('emits on loading$ on initial load and on updates', async () => { const expressionLoader = new ExpressionLoader(element, expressionString, {}); - let loadingPromise = expressionLoader.loading$.pipe(first()).toPromise(); + const loadingPromise = expressionLoader.loading$.pipe(toArray()).toPromise(); expressionLoader.update('test'); - let response = await loadingPromise; - expect(response).toBeUndefined(); - loadingPromise = expressionLoader.loading$.pipe(first()).toPromise(); expressionLoader.update(''); - response = await loadingPromise; - expect(response).toBeUndefined(); - loadingPromise = expressionLoader.loading$.pipe(first()).toPromise(); expressionLoader.update(); - response = await loadingPromise; - expect(response).toBeUndefined(); + expressionLoader.destroy(); + expect(await loadingPromise).toHaveLength(4); }); it('emits on render$ when rendering is done', async () => { diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 200249b60c773..0342713f7627b 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -17,8 +17,8 @@ * under the License. */ -import { Observable, Subject } from 'rxjs'; -import { share } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { Adapters, InspectorSession } from '../../inspector/public'; import { ExpressionDataHandler } from './execute'; import { ExpressionRenderHandler } from './render'; @@ -36,7 +36,7 @@ export class ExpressionLoader { private dataHandler: ExpressionDataHandler | undefined; private renderHandler: ExpressionRenderHandler; private dataSubject: Subject; - private loadingSubject: Subject; + private loadingSubject: Subject; private data: Data; private params: IExpressionLoaderParams = {}; @@ -46,12 +46,20 @@ export class ExpressionLoader { params?: IExpressionLoaderParams ) { this.dataSubject = new Subject(); - this.data$ = this.dataSubject.asObservable().pipe(share()); - - this.loadingSubject = new Subject(); - this.loading$ = this.loadingSubject.asObservable().pipe(share()); - - this.renderHandler = new ExpressionRenderHandler(element); + this.data$ = this.dataSubject.asObservable(); + + this.loadingSubject = new BehaviorSubject(false); + // loading is a "hot" observable, + // as loading$ could emit straight away in the constructor + // and we want to notify subscribers about it, but all subscriptions will happen later + this.loading$ = this.loadingSubject.asObservable().pipe( + filter(_ => _ === true), + map(() => void 0) + ); + + this.renderHandler = new ExpressionRenderHandler(element, { + onRenderError: params && params.onRenderError, + }); this.render$ = this.renderHandler.render$; this.update$ = this.renderHandler.update$; this.events$ = this.renderHandler.events$; @@ -64,9 +72,14 @@ export class ExpressionLoader { this.render(data); }); + this.render$.subscribe(() => { + this.loadingSubject.next(false); + }); + this.setParams(params); if (expression) { + this.loadingSubject.next(true); this.loadData(expression, this.params); } } @@ -120,7 +133,7 @@ export class ExpressionLoader { update(expression?: string | ExpressionAST, params?: IExpressionLoaderParams): void { this.setParams(params); - this.loadingSubject.next(); + this.loadingSubject.next(true); if (expression) { this.loadData(expression, this.params); } else if (this.data) { diff --git a/src/plugins/expressions/public/plugin.ts b/src/plugins/expressions/public/plugin.ts index 3a28256d57162..7471326cdd749 100644 --- a/src/plugins/expressions/public/plugin.ts +++ b/src/plugins/expressions/public/plugin.ts @@ -21,7 +21,13 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../.. import { ExpressionInterpretWithHandlers, ExpressionExecutor } from './types'; import { FunctionsRegistry, RenderFunctionsRegistry, TypesRegistry } from './registries'; import { Setup as InspectorSetup, Start as InspectorStart } from '../../inspector/public'; -import { setCoreStart, setInspector, setInterpreter, setRenderersRegistry } from './services'; +import { + setCoreStart, + setInspector, + setInterpreter, + setRenderersRegistry, + setNotifications, +} from './services'; import { clog as clogFunction } from './functions/clog'; import { font as fontFunction } from './functions/font'; import { kibana as kibanaFunction } from './functions/kibana'; @@ -158,6 +164,7 @@ export class ExpressionsPublicPlugin public start(core: CoreStart, { inspector }: ExpressionsStartDeps): ExpressionsStart { setCoreStart(core); setInspector(inspector); + setNotifications(core.notifications); return { execute, diff --git a/src/plugins/expressions/public/render.test.ts b/src/plugins/expressions/public/render.test.ts index 6b5acc8405fd2..56eb43a9bd133 100644 --- a/src/plugins/expressions/public/render.test.ts +++ b/src/plugins/expressions/public/render.test.ts @@ -17,14 +17,18 @@ * under the License. */ -import { render, ExpressionRenderHandler } from './render'; +import { ExpressionRenderHandler, render } from './render'; import { Observable } from 'rxjs'; -import { IInterpreterRenderHandlers } from './types'; +import { IInterpreterRenderHandlers, RenderError } from './types'; import { getRenderersRegistry } from './services'; -import { first } from 'rxjs/operators'; +import { first, take, toArray } from 'rxjs/operators'; const element: HTMLElement = {} as HTMLElement; - +const mockNotificationService = { + toasts: { + addError: jest.fn(() => {}), + }, +}; jest.mock('./services', () => { const renderers: Record = { test: { @@ -38,9 +42,24 @@ jest.mock('./services', () => { getRenderersRegistry: jest.fn(() => ({ get: jest.fn((id: string) => renderers[id]), })), + getNotifications: jest.fn(() => { + return mockNotificationService; + }), }; }); +const mockMockErrorRenderFunction = jest.fn( + (el: HTMLElement, error: RenderError, handlers: IInterpreterRenderHandlers) => handlers.done() +); +// extracts data from mockMockErrorRenderFunction call to assert in tests +const getHandledError = () => { + try { + return mockMockErrorRenderFunction.mock.calls[0][1]; + } catch (e) { + return null; + } +}; + describe('render helper function', () => { it('returns ExpressionRenderHandler instance', () => { const response = render(element, {}); @@ -62,40 +81,33 @@ describe('ExpressionRenderHandler', () => { }); describe('render()', () => { - it('sends an observable error and keeps it open if invalid data is provided', async () => { + beforeEach(() => { + mockMockErrorRenderFunction.mockClear(); + mockNotificationService.toasts.addError.mockClear(); + }); + + it('in case of error render$ should emit when error renderer is finished', async () => { const expressionRenderHandler = new ExpressionRenderHandler(element); - const promise1 = expressionRenderHandler.render$.pipe(first()).toPromise(); expressionRenderHandler.render(false); - await expect(promise1).resolves.toEqual({ - type: 'error', - error: { - message: 'invalid data provided to the expression renderer', - }, - }); + const promise1 = expressionRenderHandler.render$.pipe(first()).toPromise(); + await expect(promise1).resolves.toEqual(1); - const promise2 = expressionRenderHandler.render$.pipe(first()).toPromise(); expressionRenderHandler.render(false); - await expect(promise2).resolves.toEqual({ - type: 'error', - error: { - message: 'invalid data provided to the expression renderer', - }, - }); + const promise2 = expressionRenderHandler.render$.pipe(first()).toPromise(); + await expect(promise2).resolves.toEqual(2); }); - it('sends an observable error if renderer does not exist', async () => { - const expressionRenderHandler = new ExpressionRenderHandler(element); - const promise = expressionRenderHandler.render$.pipe(first()).toPromise(); - expressionRenderHandler.render({ type: 'render', as: 'something' }); - await expect(promise).resolves.toEqual({ - type: 'error', - error: { - message: `invalid renderer id 'something'`, - }, + it('should use custom error handler if provided', async () => { + const expressionRenderHandler = new ExpressionRenderHandler(element, { + onRenderError: mockMockErrorRenderFunction, }); + await expressionRenderHandler.render(false); + expect(getHandledError()!.message).toEqual( + `invalid data provided to the expression renderer` + ); }); - it('sends an observable error if the rendering function throws', async () => { + it('should throw error if the rendering function throws', async () => { (getRenderersRegistry as jest.Mock).mockReturnValueOnce({ get: () => true }); (getRenderersRegistry as jest.Mock).mockReturnValueOnce({ get: () => ({ @@ -105,15 +117,11 @@ describe('ExpressionRenderHandler', () => { }), }); - const expressionRenderHandler = new ExpressionRenderHandler(element); - const promise = expressionRenderHandler.render$.pipe(first()).toPromise(); - expressionRenderHandler.render({ type: 'render', as: 'something' }); - await expect(promise).resolves.toEqual({ - type: 'error', - error: { - message: 'renderer error', - }, + const expressionRenderHandler = new ExpressionRenderHandler(element, { + onRenderError: mockMockErrorRenderFunction, }); + await expressionRenderHandler.render({ type: 'render', as: 'something' }); + expect(getHandledError()!.message).toEqual('renderer error'); }); it('sends a next observable once rendering is complete', () => { @@ -129,18 +137,56 @@ describe('ExpressionRenderHandler', () => { }); }); + it('default renderer should use notification service', async () => { + const expressionRenderHandler = new ExpressionRenderHandler(element); + const promise1 = expressionRenderHandler.render$.pipe(first()).toPromise(); + expressionRenderHandler.render(false); + await expect(promise1).resolves.toEqual(1); + expect(mockNotificationService.toasts.addError).toBeCalledWith( + expect.objectContaining({ + message: 'invalid data provided to the expression renderer', + }), + { + title: 'Error in visualisation', + toastMessage: 'invalid data provided to the expression renderer', + } + ); + }); + // in case render$ subscription happen after render() got called // we still want to be notified about sync render$ updates it("doesn't swallow sync render errors", async () => { + const expressionRenderHandler1 = new ExpressionRenderHandler(element, { + onRenderError: mockMockErrorRenderFunction, + }); + expressionRenderHandler1.render(false); + const renderPromiseAfterRender = expressionRenderHandler1.render$.pipe(first()).toPromise(); + await expect(renderPromiseAfterRender).resolves.toEqual(1); + expect(getHandledError()!.message).toEqual( + 'invalid data provided to the expression renderer' + ); + + mockMockErrorRenderFunction.mockClear(); + + const expressionRenderHandler2 = new ExpressionRenderHandler(element, { + onRenderError: mockMockErrorRenderFunction, + }); + const renderPromiseBeforeRender = expressionRenderHandler2.render$.pipe(first()).toPromise(); + expressionRenderHandler2.render(false); + await expect(renderPromiseBeforeRender).resolves.toEqual(1); + expect(getHandledError()!.message).toEqual( + 'invalid data provided to the expression renderer' + ); + }); + + // it is expected side effect of using BehaviorSubject for render$, + // that observables will emit previous result if subscription happens after render + it('should emit previous render and error results', async () => { const expressionRenderHandler = new ExpressionRenderHandler(element); expressionRenderHandler.render(false); - const promise = expressionRenderHandler.render$.pipe(first()).toPromise(); - await expect(promise).resolves.toEqual({ - type: 'error', - error: { - message: 'invalid data provided to the expression renderer', - }, - }); + const renderPromise = expressionRenderHandler.render$.pipe(take(2), toArray()).toPromise(); + expressionRenderHandler.render(false); + await expect(renderPromise).resolves.toEqual([1, 2]); }); }); }); diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 3c7008806e779..62bde12490fbe 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -17,48 +17,58 @@ * under the License. */ -import { Observable } from 'rxjs'; import * as Rx from 'rxjs'; -import { filter, share } from 'rxjs/operators'; -import { event, RenderId, Data, IInterpreterRenderHandlers } from './types'; +import { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { + Data, + event, + IInterpreterRenderHandlers, + RenderError, + RenderErrorHandlerFnType, + RenderId, +} from './types'; import { getRenderersRegistry } from './services'; - -interface RenderError { - type: 'error'; - error: { type?: string; message: string }; -} +import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler'; export type IExpressionRendererExtraHandlers = Record; -export type RenderResult = RenderId | RenderError; +export interface ExpressionRenderHandlerParams { + onRenderError: RenderErrorHandlerFnType; +} export class ExpressionRenderHandler { - render$: Observable; + render$: Observable; update$: Observable; events$: Observable; private element: HTMLElement; private destroyFn?: any; private renderCount: number = 0; - private renderSubject: Rx.BehaviorSubject; + private renderSubject: Rx.BehaviorSubject; private eventsSubject: Rx.Subject; private updateSubject: Rx.Subject; private handlers: IInterpreterRenderHandlers; + private onRenderError: RenderErrorHandlerFnType; - constructor(element: HTMLElement) { + constructor( + element: HTMLElement, + { onRenderError }: Partial = {} + ) { this.element = element; this.eventsSubject = new Rx.Subject(); - this.events$ = this.eventsSubject.asObservable().pipe(share()); + this.events$ = this.eventsSubject.asObservable(); + + this.onRenderError = onRenderError || defaultRenderErrorHandler; - this.renderSubject = new Rx.BehaviorSubject(null as RenderResult | null); - this.render$ = this.renderSubject.asObservable().pipe( - share(), - filter(_ => _ !== null) - ) as Observable; + this.renderSubject = new Rx.BehaviorSubject(null as RenderId | null); + this.render$ = this.renderSubject.asObservable().pipe(filter(_ => _ !== null)) as Observable< + RenderId + >; this.updateSubject = new Rx.Subject(); - this.update$ = this.updateSubject.asObservable().pipe(share()); + this.update$ = this.updateSubject.asObservable(); this.handlers = { onDestroy: (fn: any) => { @@ -82,33 +92,21 @@ export class ExpressionRenderHandler { render = async (data: Data, extraHandlers: IExpressionRendererExtraHandlers = {}) => { if (!data || typeof data !== 'object') { - this.renderSubject.next({ - type: 'error', - error: { - message: 'invalid data provided to the expression renderer', - }, - }); - return; + return this.handleRenderError(new Error('invalid data provided to the expression renderer')); } if (data.type !== 'render' || !data.as) { if (data.type === 'error') { - this.renderSubject.next(data); + return this.handleRenderError(data.error); } else { - this.renderSubject.next({ - type: 'error', - error: { message: 'invalid data provided to the expression renderer' }, - }); + return this.handleRenderError( + new Error('invalid data provided to the expression renderer') + ); } - return; } if (!getRenderersRegistry().get(data.as)) { - this.renderSubject.next({ - type: 'error', - error: { message: `invalid renderer id '${data.as}'` }, - }); - return; + return this.handleRenderError(new Error(`invalid renderer id '${data.as}'`)); } try { @@ -117,10 +115,7 @@ export class ExpressionRenderHandler { .get(data.as)! .render(this.element, data.value, { ...this.handlers, ...extraHandlers }); } catch (e) { - this.renderSubject.next({ - type: 'error', - error: { type: e.type, message: e.message }, - }); + return this.handleRenderError(e); } }; @@ -136,10 +131,18 @@ export class ExpressionRenderHandler { getElement = () => { return this.element; }; + + handleRenderError = (error: RenderError) => { + this.onRenderError(this.element, error, this.handlers); + }; } -export function render(element: HTMLElement, data: Data): ExpressionRenderHandler { - const handler = new ExpressionRenderHandler(element); +export function render( + element: HTMLElement, + data: Data, + options?: Partial +): ExpressionRenderHandler { + const handler = new ExpressionRenderHandler(element, options); handler.render(data); return handler; } diff --git a/src/plugins/expressions/public/render_error_handler.ts b/src/plugins/expressions/public/render_error_handler.ts new file mode 100644 index 0000000000000..4d6bee1e375e0 --- /dev/null +++ b/src/plugins/expressions/public/render_error_handler.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { RenderErrorHandlerFnType, IInterpreterRenderHandlers, RenderError } from './types'; +import { getNotifications } from './services'; + +export const renderErrorHandler: RenderErrorHandlerFnType = ( + element: HTMLElement, + error: RenderError, + handlers: IInterpreterRenderHandlers +) => { + getNotifications().toasts.addError(error, { + title: i18n.translate('expressions.defaultErrorRenderer.errorTitle', { + defaultMessage: 'Error in visualisation', + }), + toastMessage: error.message, + }); + handlers.done(); +}; diff --git a/src/plugins/expressions/public/services.ts b/src/plugins/expressions/public/services.ts index a1a42aa85e670..75ec4826ea45a 100644 --- a/src/plugins/expressions/public/services.ts +++ b/src/plugins/expressions/public/services.ts @@ -17,6 +17,7 @@ * under the License. */ +import { NotificationsStart } from 'kibana/public'; import { createKibanaUtilsCore, createGetterSetter } from '../../kibana_utils/public'; import { ExpressionInterpreter } from './types'; import { Start as IInspector } from '../../inspector/public'; @@ -29,6 +30,9 @@ export const [getInspector, setInspector] = createGetterSetter('Insp export const [getInterpreter, setInterpreter] = createGetterSetter( 'Interpreter' ); +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); export const [getRenderersRegistry, setRenderersRegistry] = createGetterSetter< ExpressionsSetup['__LEGACY']['renderers'] diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index d86e042bca15c..66a3da48dbee9 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -20,6 +20,7 @@ import { ExpressionInterpret } from '../interpreter_provider'; import { TimeRange, Query, esFilters } from '../../../data/public'; import { Adapters } from '../../../inspector/public'; +import { ExpressionRenderDefinition } from '../registries'; export type ExpressionInterpretWithHandlers = ( ast: Parameters[0], @@ -58,6 +59,7 @@ export interface IExpressionLoaderParams { customRenderers?: []; extraHandlers?: Record; inspectorAdapters?: Adapters; + onRenderError?: RenderErrorHandlerFnType; } export interface IInterpreterHandlers { @@ -99,3 +101,15 @@ export interface IInterpreterSuccessResult { } export type IInterpreterResult = IInterpreterSuccessResult & IInterpreterErrorResult; + +export { ExpressionRenderDefinition }; + +export interface RenderError extends Error { + type?: string; +} + +export type RenderErrorHandlerFnType = ( + domNode: HTMLElement, + error: RenderError, + handlers: IInterpreterRenderHandlers +) => void; diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index 2d82f646c827b..46f330ea0a2c5 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -24,4 +24,4 @@ export * from './overlays'; export * from './ui_settings'; export * from './field_icon'; export * from './table_list_view'; -export { toMountPoint } from './util'; +export { toMountPoint, useShallowCompareEffect } from './util'; diff --git a/src/plugins/kibana_react/public/util/index.ts b/src/plugins/kibana_react/public/util/index.ts index 1053ca01603e3..4f64d6c9c81ab 100644 --- a/src/plugins/kibana_react/public/util/index.ts +++ b/src/plugins/kibana_react/public/util/index.ts @@ -20,3 +20,4 @@ export * from './use_observable'; export * from './use_unmount'; export * from './react_mount'; +export * from './use_shallow_compare_effect'; diff --git a/src/plugins/kibana_react/public/util/use_shallow_compare_effect.test.ts b/src/plugins/kibana_react/public/util/use_shallow_compare_effect.test.ts new file mode 100644 index 0000000000000..e5d9c44727c3a --- /dev/null +++ b/src/plugins/kibana_react/public/util/use_shallow_compare_effect.test.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook } from 'react-hooks-testing-library'; +import { useShallowCompareEffect } from './use_shallow_compare_effect'; + +describe('useShallowCompareEffect', () => { + test("doesn't run effect on shallow change", () => { + const callback = jest.fn(); + let deps = [1, { a: 'b' }, true]; + const { rerender } = renderHook(() => useShallowCompareEffect(callback, deps)); + + expect(callback).toHaveBeenCalledTimes(1); + callback.mockClear(); + + // no change + rerender(); + expect(callback).toHaveBeenCalledTimes(0); + callback.mockClear(); + + // no-change (new object with same properties) + deps = [1, { a: 'b' }, true]; + rerender(); + expect(callback).toHaveBeenCalledTimes(0); + callback.mockClear(); + + // change (new primitive value) + deps = [2, { a: 'b' }, true]; + rerender(); + expect(callback).toHaveBeenCalledTimes(1); + callback.mockClear(); + + // no-change + rerender(); + expect(callback).toHaveBeenCalledTimes(0); + callback.mockClear(); + + // change (new primitive value) + deps = [1, { a: 'b' }, false]; + rerender(); + expect(callback).toHaveBeenCalledTimes(1); + callback.mockClear(); + + // change (new properties on object) + deps = [1, { a: 'c' }, false]; + rerender(); + expect(callback).toHaveBeenCalledTimes(1); + callback.mockClear(); + }); + + test('runs effect on deep change', () => { + const callback = jest.fn(); + let deps = [1, { a: { b: 'c' } }, true]; + const { rerender } = renderHook(() => useShallowCompareEffect(callback, deps)); + + expect(callback).toHaveBeenCalledTimes(1); + callback.mockClear(); + + // no change + rerender(); + expect(callback).toHaveBeenCalledTimes(0); + callback.mockClear(); + + // change (new nested object ) + deps = [1, { a: { b: 'c' } }, true]; + rerender(); + expect(callback).toHaveBeenCalledTimes(1); + callback.mockClear(); + }); +}); diff --git a/src/plugins/kibana_react/public/util/use_shallow_compare_effect.ts b/src/plugins/kibana_react/public/util/use_shallow_compare_effect.ts new file mode 100644 index 0000000000000..dfba7b907f5fb --- /dev/null +++ b/src/plugins/kibana_react/public/util/use_shallow_compare_effect.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useRef } from 'react'; + +/** + * Similar to https://github.com/kentcdodds/use-deep-compare-effect + * but uses shallow compare instead of deep + */ +export function useShallowCompareEffect( + callback: React.EffectCallback, + deps: React.DependencyList +) { + useEffect(callback, useShallowCompareMemoize(deps)); +} +function useShallowCompareMemoize(deps: React.DependencyList) { + const ref = useRef(undefined); + + if (!ref.current || deps.some((dep, index) => !shallowEqual(dep, ref.current![index]))) { + ref.current = deps; + } + + return ref.current; +} +// https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/shallowEqual.js +function shallowEqual(objA: any, objB: any): boolean { + if (is(objA, objB)) { + return true; + } + + if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { + return false; + } + + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + // Test for A's keys different from B. + for (let i = 0; i < keysA.length; i++) { + if ( + !Object.prototype.hasOwnProperty.call(objB, keysA[i]) || + !is(objA[keysA[i]], objB[keysA[i]]) + ) { + return false; + } + } + + return true; +} + +/** + * IE11 does not support Object.is + */ +function is(x: any, y: any): boolean { + if (x === y) { + return x !== 0 || y !== 0 || 1 / x === 1 / y; + } else { + return x !== x && y !== y; + } +} diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx index c091765619a19..daa19f22a7023 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/app/components/main.tsx @@ -29,7 +29,7 @@ import { Context, ExpressionRenderHandler, ExpressionDataHandler, - RenderResult, + RenderId, } from '../../types'; import { getExpressions } from '../../services'; @@ -40,7 +40,7 @@ declare global { context?: Context, initialContext?: Context ) => ReturnType; - renderPipelineResponse: (context?: Context) => Promise; + renderPipelineResponse: (context?: Context) => Promise; } } @@ -85,16 +85,16 @@ class Main extends React.Component<{}, State> { lastRenderHandler.destroy(); } - lastRenderHandler = getExpressions().render(this.chartRef.current!, context); - const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise(); + lastRenderHandler = getExpressions().render(this.chartRef.current!, context, { + onRenderError: (el, error, handler) => { + this.setState({ + expression: 'Render error!\n\n' + JSON.stringify(error), + }); + handler.done(); + }, + }); - if (typeof renderResult === 'object' && renderResult.type === 'error') { - this.setState({ - expression: 'Render error!\n\n' + JSON.stringify(renderResult.error), - }); - } - - return renderResult; + return lastRenderHandler.render$.pipe(first()).toPromise(); }; } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts index 082bb47d80066..cc4190bd099fa 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/types.ts @@ -22,7 +22,7 @@ import { Context, ExpressionRenderHandler, ExpressionDataHandler, - RenderResult, + RenderId, } from 'src/plugins/expressions/public'; import { Adapters } from 'src/plugins/inspector/public'; @@ -32,6 +32,6 @@ export { Context, ExpressionRenderHandler, ExpressionDataHandler, - RenderResult, + RenderId, Adapters, }; diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts index e1ec18fae5e3a..7fedf1723908a 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -21,8 +21,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../functional/ftr_provider_context'; import { ExpressionDataHandler, - RenderResult, Context, + RenderId, } from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types'; type UnWrapPromise = T extends Promise ? U : T; @@ -168,8 +168,8 @@ export function expectExpressionProvider({ toMatchScreenshot: async () => { const pipelineResponse = await handler.getResponse(); log.debug('starting to render'); - const result = await browser.executeAsync( - (_context: ExpressionResult, done: (renderResult: RenderResult) => void) => + const result = await browser.executeAsync( + (_context: ExpressionResult, done: (renderResult: RenderId) => void) => window.renderPipelineResponse(_context).then(renderResult => { done(renderResult); return renderResult; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx index 21a69bfc3a0b3..3dd4373347129 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx @@ -50,6 +50,7 @@ export function ExpressionWrapper({ padding="m" expression={expression} searchContext={{ ...context, type: 'kibana_context' }} + renderError={error =>
    {error}
    } />
    )} From 0cc4060d4f8cb4f3fe9c8b19ec20e3100c2f248d Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Wed, 27 Nov 2019 05:12:05 -0500 Subject: [PATCH 108/128] shim visualizations plugin (#50624) --- .../data/public/search/expressions/esaggs.ts | 2 +- .../input_control_vis/public/register_vis.js | 95 +++--- .../kbn_vislib_vis_types/public/area.js | 268 +++++++++-------- .../kbn_vislib_vis_types/public/gauge.js | 161 +++++------ .../kbn_vislib_vis_types/public/goal.js | 154 +++++----- .../kbn_vislib_vis_types/public/heatmap.js | 164 ++++++----- .../kbn_vislib_vis_types/public/histogram.js | 270 +++++++++--------- .../public/horizontal_bar.js | 268 +++++++++-------- .../public/kbn_vislib_vis_types.js | 32 +-- .../kbn_vislib_vis_types/public/line.js | 264 +++++++++-------- .../kbn_vislib_vis_types/public/pie.js | 120 ++++---- .../kibana/public/discover/kibana_services.ts | 2 +- .../saved_visualizations/_saved_vis.js | 2 +- .../wizard/type_selection/type_selection.tsx | 2 +- .../__tests__/region_map_visualization.js | 2 +- .../core_plugins/region_map/public/plugin.ts | 2 +- .../region_map/public/region_map_type.js | 8 +- .../coordinate_maps_visualization.js | 2 +- .../core_plugins/tile_map/public/plugin.ts | 2 +- .../tile_map/public/tile_map_type.js | 7 +- .../core_plugins/timelion/public/plugin.ts | 2 +- .../core_plugins/timelion/public/vis/index.ts | 5 +- .../vis_type_markdown/public/markdown_vis.ts | 6 +- .../vis_type_markdown/public/plugin.ts | 4 +- .../public/__tests__/metric_vis.js | 4 +- .../vis_type_metric/public/metric_vis_type.ts | 179 ++++++------ .../vis_type_metric/public/plugin.ts | 4 +- .../vis_type_metric/public/types.ts | 2 +- .../public/__tests__/table_vis_controller.js | 17 +- .../public/agg_table/__tests__/agg_table.js | 15 +- .../vis_type_table/public/plugin.ts | 4 +- .../vis_type_table/public/table_vis_type.ts | 131 +++++---- .../vis_type_table/public/types.ts | 2 +- .../vis_type_tagcloud/public/plugin.ts | 4 +- .../public/tag_cloud_type.ts | 177 ++++++------ .../public/editor_controller.js | 103 ++++--- .../vis_type_timeseries/public/metrics_fn.ts | 6 +- .../public/metrics_type.ts | 122 ++++---- .../vis_type_timeseries/public/plugin.ts | 21 +- .../public/request_handler.js | 71 +++-- .../vis_type_timeseries/public/services.ts} | 24 +- .../public/__tests__/vega_visualization.js | 4 +- .../vis_type_vega/public/plugin.ts | 2 +- .../vis_type_vega/public/vega_type.ts | 7 +- .../expressions/visualization_renderer.tsx | 2 +- .../visualizations/public/index.ts | 18 -- .../visualizations/public/legacy_imports.ts} | 19 +- .../visualizations/public/legacy_mocks.ts} | 2 +- .../visualization_noresults.test.js.snap | 0 .../visualization_requesterror.test.js.snap | 0 .../np_ready/public/components/_index.scss | 1 + .../public}/components/_visualization.scss | 0 .../np_ready/public}/components/index.ts | 0 .../public}/components/visualization.test.js | 0 .../public}/components/visualization.tsx | 6 +- .../components/visualization_chart.test.js | 0 .../components/visualization_chart.tsx | 8 +- .../visualization_noresults.test.js | 0 .../components/visualization_noresults.tsx | 4 - .../visualization_requesterror.test.js | 0 .../components/visualization_requesterror.tsx | 6 +- .../np_ready/public/filters}/brush_event.js | 2 +- .../public/filters}/brush_event.test.js | 0 .../public/filters}/brush_event.test.mocks.ts | 2 +- .../public/np_ready/public/filters/index.ts | 3 +- .../np_ready/public/filters}/vis_filters.js | 22 +- .../public/np_ready/public/index.ts | 10 +- .../public/np_ready/public/legacy.ts | 9 +- .../__snapshots__/build_pipeline.test.ts.snap | 0 .../np_ready/public/legacy}/__tests__/_vis.js | 4 +- .../__tests__/vis_types/base_vis_type.js | 2 +- .../__tests__/vis_types/react_vis_type.js | 2 +- .../__tests__/vis_update_objs/gauge_objs.js | 0 .../public/legacy}/build_pipeline.test.ts | 6 +- .../np_ready/public/legacy}/build_pipeline.ts | 14 +- .../public/legacy}/calculate_object_hash.d.ts | 0 .../public/legacy}/calculate_object_hash.js | 0 .../np_ready/public/legacy}/memoize.test.ts | 0 .../public/np_ready/public/legacy}/memoize.ts | 0 .../public/legacy}/update_status.test.js | 0 .../np_ready/public/legacy}/update_status.ts | 6 +- .../np_ready/public/legacy}/vis_update.js | 0 .../public/legacy}/vis_update_state.js | 0 .../public/legacy}/vis_update_state.test.js | 0 .../public/np_ready/public/mocks.ts | 19 +- .../public/np_ready/public/plugin.ts | 36 +-- .../filters_service.ts => services.ts} | 30 +- .../np_ready/public/types}/base_vis_type.js | 2 +- .../np_ready/public/types}/react_vis_type.js | 8 +- .../np_ready/public/types/types_service.ts | 33 ++- .../public/np_ready/public}/vis.d.ts | 12 +- .../public/np_ready/public}/vis.js | 17 +- src/legacy/ui/public/agg_types/agg_configs.ts | 3 +- .../agg_types/buckets/date_histogram.ts | 4 +- src/legacy/ui/public/chrome/api/angular.js | 3 +- .../inspector/build_tabular_inspector_data.ts | 2 +- .../ui/public/time_buckets/time_buckets.js | 13 +- src/legacy/ui/public/vis/__tests__/index.js | 3 - src/legacy/ui/public/vis/index.d.ts | 3 +- src/legacy/ui/public/vis/index.js | 2 +- .../vis_types/__tests__/vislib_vis_legend.js | 2 +- src/legacy/ui/public/vis/vis_types/index.js | 4 +- .../ui/public/vis/vis_types/vis_type.ts | 29 -- .../public/vis/vis_types/vislib_vis_legend.js | 2 +- src/legacy/ui/public/visualize/_index.scss | 2 +- .../public/visualize/components/_index.scss | 1 - .../loader/pipeline_helpers/index.ts | 2 +- .../loader/pipeline_helpers/utilities.ts | 5 +- .../self_changing_vis/self_changing_vis.js | 48 ++-- .../test_suites/core_plugins/applications.ts | 4 +- .../definitions/date_histogram.test.tsx | 19 +- .../lens/public/register_vis_type_alias.ts | 2 +- 112 files changed, 1509 insertions(+), 1692 deletions(-) rename src/legacy/{ui/public/vis/vis_factory.js => core_plugins/vis_type_timeseries/public/services.ts} (62%) rename src/legacy/{ui/public/vis/push_filters.js => core_plugins/visualizations/public/legacy_imports.ts} (56%) rename src/legacy/{ui/public/vis/vis_filters/index.js => core_plugins/visualizations/public/legacy_mocks.ts} (88%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/__snapshots__/visualization_noresults.test.js.snap (100%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/__snapshots__/visualization_requesterror.test.js.snap (100%) create mode 100644 src/legacy/core_plugins/visualizations/public/np_ready/public/components/_index.scss rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/_visualization.scss (100%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/index.ts (100%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/visualization.test.js (100%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/visualization.tsx (95%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/visualization_chart.test.js (100%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/visualization_chart.tsx (95%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/visualization_noresults.test.js (100%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/visualization_noresults.tsx (90%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/visualization_requesterror.test.js (100%) rename src/legacy/{ui/public/visualize => core_plugins/visualizations/public/np_ready/public}/components/visualization_requesterror.tsx (88%) rename src/legacy/{ui/public/vis/vis_filters => core_plugins/visualizations/public/np_ready/public/filters}/brush_event.js (96%) rename src/legacy/{ui/public/vis/vis_filters => core_plugins/visualizations/public/np_ready/public/filters}/brush_event.test.js (100%) rename src/legacy/{ui/public/vis/vis_filters => core_plugins/visualizations/public/np_ready/public/filters}/brush_event.test.mocks.ts (92%) rename src/legacy/{ui/public/vis/vis_filters => core_plugins/visualizations/public/np_ready/public/filters}/vis_filters.js (84%) rename src/legacy/{ui/public/visualize/loader/pipeline_helpers => core_plugins/visualizations/public/np_ready/public/legacy}/__snapshots__/build_pipeline.test.ts.snap (100%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/__tests__/_vis.js (96%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/__tests__/vis_types/base_vis_type.js (96%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/__tests__/vis_types/react_vis_type.js (96%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/__tests__/vis_update_objs/gauge_objs.js (100%) rename src/legacy/{ui/public/visualize/loader/pipeline_helpers => core_plugins/visualizations/public/np_ready/public/legacy}/build_pipeline.test.ts (98%) rename src/legacy/{ui/public/visualize/loader/pipeline_helpers => core_plugins/visualizations/public/np_ready/public/legacy}/build_pipeline.ts (98%) rename src/legacy/{ui/public/vis/lib => core_plugins/visualizations/public/np_ready/public/legacy}/calculate_object_hash.d.ts (100%) rename src/legacy/{ui/public/vis/lib => core_plugins/visualizations/public/np_ready/public/legacy}/calculate_object_hash.js (100%) rename src/legacy/{ui/public/utils => core_plugins/visualizations/public/np_ready/public/legacy}/memoize.test.ts (100%) rename src/legacy/{ui/public/utils => core_plugins/visualizations/public/np_ready/public/legacy}/memoize.ts (100%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/update_status.test.js (100%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/update_status.ts (95%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/vis_update.js (100%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/vis_update_state.js (100%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public/legacy}/vis_update_state.test.js (100%) rename src/legacy/core_plugins/visualizations/public/np_ready/public/{filters/filters_service.ts => services.ts} (63%) rename src/legacy/{ui/public/vis/vis_types => core_plugins/visualizations/public/np_ready/public/types}/base_vis_type.js (97%) rename src/legacy/{ui/public/vis/vis_types => core_plugins/visualizations/public/np_ready/public/types}/react_vis_type.js (93%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public}/vis.d.ts (76%) rename src/legacy/{ui/public/vis => core_plugins/visualizations/public/np_ready/public}/vis.js (91%) delete mode 100644 src/legacy/ui/public/vis/vis_types/vis_type.ts delete mode 100644 src/legacy/ui/public/visualize/components/_index.scss diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 7165de026920d..61b9b7bf83c03 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -42,7 +42,7 @@ import { } from '../../../../../ui/public/filter_manager/query_filter'; import { buildTabularInspectorData } from '../../../../../ui/public/inspector/build_tabular_inspector_data'; -import { calculateObjectHash } from '../../../../../ui/public/vis/lib/calculate_object_hash'; +import { calculateObjectHash } from '../../../../visualizations/public'; import { getTime } from '../../../../../ui/public/timefilter'; // @ts-ignore import { tabifyAggResponse } from '../../../../../ui/public/agg_response/tabify/tabify'; diff --git a/src/legacy/core_plugins/input_control_vis/public/register_vis.js b/src/legacy/core_plugins/input_control_vis/public/register_vis.js index 731cf2dac9dd2..12e7291fea7a1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/register_vis.js +++ b/src/legacy/core_plugins/input_control_vis/public/register_vis.js @@ -17,65 +17,58 @@ * under the License. */ -import { visFactory } from 'ui/vis/vis_factory'; import { VisController } from './vis_controller'; import { ControlsTab } from './components/editor/controls_tab'; import { OptionsTab } from './components/editor/options_tab'; -import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; -import { Status } from 'ui/vis/update_status'; import { i18n } from '@kbn/i18n'; import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy'; +import { Status, defaultFeedbackMessage } from '../../visualizations/public'; -function InputControlVisProvider() { - // return the visType object, which kibana will use to display and configure new Vis object of this type. - return visFactory.createBaseVisualization({ - name: 'input_control_vis', - title: i18n.translate('inputControl.register.controlsTitle', { - defaultMessage: 'Controls' - }), - icon: 'visControls', - description: i18n.translate('inputControl.register.controlsDescription', { - defaultMessage: 'Create interactive controls for easy dashboard manipulation.' - }), - stage: 'experimental', - requiresUpdateStatus: [Status.PARAMS, Status.TIME], - feedbackMessage: defaultFeedbackMessage, - visualization: VisController, - visConfig: { - defaults: { - controls: [], - updateFiltersOnChange: false, - useTimeFilter: false, - pinFilters: false, - }, - }, - editor: 'default', - editorConfig: { - optionTabs: [ - { - name: 'controls', - title: i18n.translate('inputControl.register.tabs.controlsTitle', { - defaultMessage: 'Controls' - }), - editor: ControlsTab - }, - { - name: 'options', - title: i18n.translate('inputControl.register.tabs.optionsTitle', { - defaultMessage: 'Options' - }), - editor: OptionsTab - } - ] +export const inputControlVisDefinition = { + name: 'input_control_vis', + title: i18n.translate('inputControl.register.controlsTitle', { + defaultMessage: 'Controls' + }), + icon: 'visControls', + description: i18n.translate('inputControl.register.controlsDescription', { + defaultMessage: 'Create interactive controls for easy dashboard manipulation.' + }), + stage: 'experimental', + requiresUpdateStatus: [Status.PARAMS, Status.TIME], + feedbackMessage: defaultFeedbackMessage, + visualization: VisController, + visConfig: { + defaults: { + controls: [], + updateFiltersOnChange: false, + useTimeFilter: false, + pinFilters: false, }, - requestHandler: 'none', - responseHandler: 'none', - }); -} + }, + editor: 'default', + editorConfig: { + optionTabs: [ + { + name: 'controls', + title: i18n.translate('inputControl.register.tabs.controlsTitle', { + defaultMessage: 'Controls' + }), + editor: ControlsTab + }, + { + name: 'options', + title: i18n.translate('inputControl.register.tabs.optionsTitle', { + defaultMessage: 'Options' + }), + editor: OptionsTab + } + ] + }, + requestHandler: 'none', + responseHandler: 'none', +}; // register the provider with the visTypes registry -visualizations.types.registerVisualization(InputControlVisProvider); +visualizations.types.createBaseVisualization(inputControlVisDefinition); -// export the provider so that the visType can be required with Private() -export default InputControlVisProvider; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js index ca9115b729da8..a03e8affe319b 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js @@ -17,7 +17,6 @@ * under the License. */ -import { visFactory } from 'ui/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggGroupNames } from 'ui/vis/editors/default'; @@ -37,143 +36,140 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { palettes } from '@elastic/eui/lib/services'; import { vislibVisController } from './controller'; -export default function PointSeriesVisType() { - - return visFactory.createBaseVisualization({ - name: 'area', - title: i18n.translate('kbnVislibVisTypes.area.areaTitle', { defaultMessage: 'Area' }), - icon: 'visArea', - description: i18n.translate( - 'kbnVislibVisTypes.area.areaDescription', { defaultMessage: 'Emphasize the quantity beneath a line chart' }), - visualization: vislibVisController, - visConfig: { - defaults: { - type: 'area', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100 - }, - title: {} - } - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100 - }, - title: { - text: countLabel - } - } - ], - seriesParams: [ - { - show: true, - type: ChartTypes.AREA, - mode: ChartModes.STACKED, - data: { - label: countLabel, - id: '1' - }, - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - interpolate: InterpolationModes.LINEAR, - valueAxis: 'ValueAxis-1', - } - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: palettes.euiPaletteColorBlind.colors[9] - }, - labels: {} +export const areaDefinition = { + name: 'area', + title: i18n.translate('kbnVislibVisTypes.area.areaTitle', { defaultMessage: 'Area' }), + icon: 'visArea', + description: i18n.translate( + 'kbnVislibVisTypes.area.areaDescription', { defaultMessage: 'Emphasize the quantity beneath a line chart' }), + visualization: vislibVisController, + visConfig: { + defaults: { + type: 'area', + grid: { + categoryLines: false, }, - }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('kbnVislibVisTypes.area.metricsTitle', { defaultMessage: 'Y-axis' }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - min: 1, - defaults: [ - { schema: 'metric', type: 'count' } - ] - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('kbnVislibVisTypes.area.radiusTitle', { defaultMessage: 'Dot size' }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] - }, + categoryAxes: [ { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('kbnVislibVisTypes.area.segmentTitle', { defaultMessage: 'X-axis' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, + id: 'CategoryAxis-1', + type: AxisTypes.CATEGORY, + position: Positions.BOTTOM, + show: true, + style: {}, + scale: { + type: ScaleTypes.LINEAR, + }, + labels: { + show: true, + filter: true, + truncate: 100 + }, + title: {} + } + ], + valueAxes: [ { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('kbnVislibVisTypes.area.groupTitle', { defaultMessage: 'Split series' }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: AxisTypes.VALUE, + position: Positions.LEFT, + show: true, + style: {}, + scale: { + type: ScaleTypes.LINEAR, + mode: AxisModes.NORMAL, + }, + labels: { + show: true, + rotate: Rotates.HORIZONTAL, + filter: false, + truncate: 100 + }, + title: { + text: countLabel + } + } + ], + seriesParams: [ { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('kbnVislibVisTypes.area.splitTitle', { defaultMessage: 'Split chart' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + show: true, + type: ChartTypes.AREA, + mode: ChartModes.STACKED, + data: { + label: countLabel, + id: '1' + }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: InterpolationModes.LINEAR, + valueAxis: 'ValueAxis-1', } - ]) - } - }); -} + ], + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: ThresholdLineStyles.FULL, + color: palettes.euiPaletteColorBlind.colors[9] + }, + labels: {} + }, + }, + events: { + brush: { disabled: false }, + }, + editorConfig: { + collections: getConfigCollections(), + optionTabs: getAreaOptionTabs(), + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('kbnVislibVisTypes.area.metricsTitle', { defaultMessage: 'Y-axis' }), + aggFilter: ['!geo_centroid', '!geo_bounds'], + min: 1, + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: AggGroupNames.Metrics, + name: 'radius', + title: i18n.translate('kbnVislibVisTypes.area.radiusTitle', { defaultMessage: 'Dot size' }), + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('kbnVislibVisTypes.area.segmentTitle', { defaultMessage: 'X-axis' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('kbnVislibVisTypes.area.groupTitle', { defaultMessage: 'Split series' }), + min: 0, + max: 3, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('kbnVislibVisTypes.area.splitTitle', { defaultMessage: 'Split chart' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + } + ]) + } +}; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js index 75907618eb859..6d0d997604e01 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js @@ -18,7 +18,6 @@ */ import { i18n } from '@kbn/i18n'; -import { visFactory } from 'ui/vis/vis_factory'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggGroupNames } from 'ui/vis/editors/default'; import { ColorSchemas } from 'ui/vislib/components/color/colormaps'; @@ -26,87 +25,85 @@ import { GaugeOptions } from './components/options'; import { getGaugeCollections, Alignments, ColorModes, GaugeTypes } from './utils/collections'; import { vislibVisController } from './controller'; -export default function GaugeVisType() { - return visFactory.createBaseVisualization({ - name: 'gauge', - title: i18n.translate('kbnVislibVisTypes.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), - icon: 'visGauge', - description: i18n.translate('kbnVislibVisTypes.gauge.gaugeDescription', { - defaultMessage: 'Gauges indicate the status of a metric. Use it to show how a metric\'s value relates to reference threshold values.' - }), - visConfig: { - defaults: { - type: 'gauge', - addTooltip: true, - addLegend: true, - isDisplayWarning: false, - gauge: { - alignment: Alignments.AUTOMATIC, - extendRange: true, - percentageMode: false, - gaugeType: GaugeTypes.ARC, - gaugeStyle: 'Full', - backStyle: 'Full', - orientation: 'vertical', - colorSchema: ColorSchemas.GreenToRed, - gaugeColorMode: ColorModes.LABELS, - colorsRange: [ - { from: 0, to: 50 }, - { from: 50, to: 75 }, - { from: 75, to: 100 } - ], - invertColors: false, - labels: { - show: true, - color: 'black' - }, - scale: { - show: true, - labels: false, - color: 'rgba(105,112,125,0.2)', - }, - type: 'meter', - style: { - bgWidth: 0.9, - width: 0.9, - mask: false, - bgMask: false, - maskBars: 50, - bgFill: 'rgba(105,112,125,0.2)', - bgColor: true, - subText: '', - fontSize: 60, - } - } - }, - }, - visualization: vislibVisController, - editorConfig: { - collections: getGaugeCollections(), - optionsTemplate: GaugeOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('kbnVislibVisTypes.gauge.metricTitle', { defaultMessage: 'Metric' }), - min: 1, - aggFilter: [ - '!std_dev', '!geo_centroid', '!percentiles', '!percentile_ranks', - '!derivative', '!serial_diff', '!moving_avg', '!cumulative_sum', '!geo_bounds'], - defaults: [ - { schema: 'metric', type: 'count' } - ] +export const gaugeDefinition = { + name: 'gauge', + title: i18n.translate('kbnVislibVisTypes.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), + icon: 'visGauge', + description: i18n.translate('kbnVislibVisTypes.gauge.gaugeDescription', { + defaultMessage: 'Gauges indicate the status of a metric. Use it to show how a metric\'s value relates to reference threshold values.' + }), + visConfig: { + defaults: { + type: 'gauge', + addTooltip: true, + addLegend: true, + isDisplayWarning: false, + gauge: { + alignment: Alignments.AUTOMATIC, + extendRange: true, + percentageMode: false, + gaugeType: GaugeTypes.ARC, + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: ColorSchemas.GreenToRed, + gaugeColorMode: ColorModes.LABELS, + colorsRange: [ + { from: 0, to: 50 }, + { from: 50, to: 75 }, + { from: 75, to: 100 } + ], + invertColors: false, + labels: { + show: true, + color: 'black' }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('kbnVislibVisTypes.gauge.groupTitle', { defaultMessage: 'Split group' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + scale: { + show: true, + labels: false, + color: 'rgba(105,112,125,0.2)', + }, + type: 'meter', + style: { + bgWidth: 0.9, + width: 0.9, + mask: false, + bgMask: false, + maskBars: 50, + bgFill: 'rgba(105,112,125,0.2)', + bgColor: true, + subText: '', + fontSize: 60, } - ]) + } }, - useCustomNoDataScreen: true - }); -} + }, + visualization: vislibVisController, + editorConfig: { + collections: getGaugeCollections(), + optionsTemplate: GaugeOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('kbnVislibVisTypes.gauge.metricTitle', { defaultMessage: 'Metric' }), + min: 1, + aggFilter: [ + '!std_dev', '!geo_centroid', '!percentiles', '!percentile_ranks', + '!derivative', '!serial_diff', '!moving_avg', '!cumulative_sum', '!geo_bounds'], + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('kbnVislibVisTypes.gauge.groupTitle', { defaultMessage: 'Split group' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + } + ]) + }, + useCustomNoDataScreen: true +}; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/goal.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/goal.js index 3a6b9f873aa87..dedd2e3885876 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/goal.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/goal.js @@ -24,86 +24,82 @@ import { ColorSchemas } from 'ui/vislib/components/color/colormaps'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, GaugeTypes, ColorModes } from './utils/collections'; import { vislibVisController } from './controller'; -import { visFactory } from '../../../ui/public/vis/vis_factory'; -export default function GoalVisType() { - - return visFactory.createBaseVisualization({ - name: 'goal', - title: i18n.translate('kbnVislibVisTypes.goal.goalTitle', { defaultMessage: 'Goal' }), - icon: 'visGoal', - description: i18n.translate('kbnVislibVisTypes.goal.goalDescription', { - defaultMessage: 'A goal chart indicates how close you are to your final goal.' - }), - visualization: vislibVisController, - visConfig: { - defaults: { - addTooltip: true, - addLegend: false, - isDisplayWarning: false, - type: 'gauge', - gauge: { - verticalSplit: false, - autoExtend: false, - percentageMode: true, - gaugeType: GaugeTypes.ARC, - gaugeStyle: 'Full', - backStyle: 'Full', - orientation: 'vertical', - useRanges: false, - colorSchema: ColorSchemas.GreenToRed, - gaugeColorMode: ColorModes.NONE, - colorsRange: [ - { from: 0, to: 10000 } - ], - invertColors: false, - labels: { - show: true, - color: 'black' - }, - scale: { - show: false, - labels: false, - color: 'rgba(105,112,125,0.2)', - width: 2 - }, - type: 'meter', - style: { - bgFill: 'rgba(105,112,125,0.2)', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 60, - } - } - }, - }, - editorConfig: { - collections: getGaugeCollections(), - optionsTemplate: GaugeOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('kbnVislibVisTypes.goal.metricTitle', { defaultMessage: 'Metric' }), - min: 1, - aggFilter: [ - '!std_dev', '!geo_centroid', '!percentiles', '!percentile_ranks', - '!derivative', '!serial_diff', '!moving_avg', '!cumulative_sum', '!geo_bounds'], - defaults: [ - { schema: 'metric', type: 'count' } - ] +export const goalDefinition = { + name: 'goal', + title: i18n.translate('kbnVislibVisTypes.goal.goalTitle', { defaultMessage: 'Goal' }), + icon: 'visGoal', + description: i18n.translate('kbnVislibVisTypes.goal.goalDescription', { + defaultMessage: 'A goal chart indicates how close you are to your final goal.' + }), + visualization: vislibVisController, + visConfig: { + defaults: { + addTooltip: true, + addLegend: false, + isDisplayWarning: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: true, + gaugeType: GaugeTypes.ARC, + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + useRanges: false, + colorSchema: ColorSchemas.GreenToRed, + gaugeColorMode: ColorModes.NONE, + colorsRange: [ + { from: 0, to: 10000 } + ], + invertColors: false, + labels: { + show: true, + color: 'black' + }, + scale: { + show: false, + labels: false, + color: 'rgba(105,112,125,0.2)', + width: 2 }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('kbnVislibVisTypes.goal.groupTitle', { defaultMessage: 'Split group' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + type: 'meter', + style: { + bgFill: 'rgba(105,112,125,0.2)', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 60, } - ]) + } }, - useCustomNoDataScreen: true - }); -} + }, + editorConfig: { + collections: getGaugeCollections(), + optionsTemplate: GaugeOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('kbnVislibVisTypes.goal.metricTitle', { defaultMessage: 'Metric' }), + min: 1, + aggFilter: [ + '!std_dev', '!geo_centroid', '!percentiles', '!percentile_ranks', + '!derivative', '!serial_diff', '!moving_avg', '!cumulative_sum', '!geo_bounds'], + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('kbnVislibVisTypes.goal.groupTitle', { defaultMessage: 'Split group' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + } + ]) + }, + useCustomNoDataScreen: true +}; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/heatmap.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/heatmap.js index 207f80996b5a7..e3212037ecf2f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/heatmap.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/heatmap.js @@ -17,7 +17,6 @@ * under the License. */ -import { visFactory } from '../../../ui/public/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggGroupNames } from 'ui/vis/editors/default'; @@ -26,89 +25,86 @@ import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils import { HeatmapOptions } from './components/options'; import { vislibVisController } from './controller'; -export default function HeatmapVisType() { - - return visFactory.createBaseVisualization({ - name: 'heatmap', - title: i18n.translate('kbnVislibVisTypes.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), - icon: 'visHeatmap', - description: i18n.translate('kbnVislibVisTypes.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix' }), - visualization: vislibVisController, - visConfig: { - defaults: { - type: 'heatmap', - addTooltip: true, - addLegend: true, - enableHover: false, - legendPosition: Positions.RIGHT, - times: [], - colorsNumber: 4, - colorSchema: ColorSchemas.Greens, - setColorRange: false, - colorsRange: [], - invertColors: false, - percentageMode: false, - valueAxes: [{ - show: false, - id: 'ValueAxis-1', - type: AxisTypes.VALUE, - scale: { - type: ScaleTypes.LINEAR, - defaultYExtents: false, - }, - labels: { - show: false, - rotate: 0, - overwriteColor: false, - color: 'black', - } - }] - }, - }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getHeatmapCollections(), - optionsTemplate: HeatmapOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('kbnVislibVisTypes.heatmap.metricTitle', { defaultMessage: 'Value' }), - min: 1, - max: 1, - aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev', 'top_hits'], - defaults: [ - { schema: 'metric', type: 'count' } - ] - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('kbnVislibVisTypes.heatmap.segmentTitle', { defaultMessage: 'X-axis' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] +export const heatmapDefinition = { + name: 'heatmap', + title: i18n.translate('kbnVislibVisTypes.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), + icon: 'visHeatmap', + description: i18n.translate('kbnVislibVisTypes.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix' }), + visualization: vislibVisController, + visConfig: { + defaults: { + type: 'heatmap', + addTooltip: true, + addLegend: true, + enableHover: false, + legendPosition: Positions.RIGHT, + times: [], + colorsNumber: 4, + colorSchema: ColorSchemas.Greens, + setColorRange: false, + colorsRange: [], + invertColors: false, + percentageMode: false, + valueAxes: [{ + show: false, + id: 'ValueAxis-1', + type: AxisTypes.VALUE, + scale: { + type: ScaleTypes.LINEAR, + defaultYExtents: false, }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('kbnVislibVisTypes.heatmap.groupTitle', { defaultMessage: 'Y-axis' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('kbnVislibVisTypes.heatmap.splitTitle', { defaultMessage: 'Split chart' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + labels: { + show: false, + rotate: 0, + overwriteColor: false, + color: 'black', } - ]) - } + }] + }, + }, + events: { + brush: { disabled: false }, + }, + editorConfig: { + collections: getHeatmapCollections(), + optionsTemplate: HeatmapOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('kbnVislibVisTypes.heatmap.metricTitle', { defaultMessage: 'Value' }), + min: 1, + max: 1, + aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev', 'top_hits'], + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('kbnVislibVisTypes.heatmap.segmentTitle', { defaultMessage: 'X-axis' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('kbnVislibVisTypes.heatmap.groupTitle', { defaultMessage: 'Y-axis' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('kbnVislibVisTypes.heatmap.splitTitle', { defaultMessage: 'Split chart' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + } + ]) + } - }); -} +}; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js index 87e690fa6457e..15ede19e21c22 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js @@ -17,7 +17,6 @@ * under the License. */ -import { visFactory } from '../../../ui/public/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggGroupNames } from 'ui/vis/editors/default'; @@ -36,146 +35,143 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { palettes } from '@elastic/eui/lib/services'; import { vislibVisController } from './controller'; -export default function PointSeriesVisType() { - - return visFactory.createBaseVisualization({ - name: 'histogram', - title: i18n.translate('kbnVislibVisTypes.histogram.histogramTitle', { defaultMessage: 'Vertical Bar' }), - icon: 'visBarVertical', - description: i18n.translate('kbnVislibVisTypes.histogram.histogramDescription', - { defaultMessage: 'Assign a continuous variable to each axis' } - ), - visualization: vislibVisController, - visConfig: { - defaults: { - type: 'histogram', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100 - }, - title: {} - } - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, +export const histogramDefinition = { + name: 'histogram', + title: i18n.translate('kbnVislibVisTypes.histogram.histogramTitle', { defaultMessage: 'Vertical Bar' }), + icon: 'visBarVertical', + description: i18n.translate('kbnVislibVisTypes.histogram.histogramDescription', + { defaultMessage: 'Assign a continuous variable to each axis' } + ), + visualization: vislibVisController, + visConfig: { + defaults: { + type: 'histogram', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: AxisTypes.CATEGORY, + position: Positions.BOTTOM, + show: true, + style: {}, + scale: { + type: ScaleTypes.LINEAR, + }, + labels: { show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100 - }, - title: { - text: countLabel, - } - } - ], - seriesParams: [ - { + filter: true, + truncate: 100 + }, + title: {} + } + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: AxisTypes.VALUE, + position: Positions.LEFT, + show: true, + style: {}, + scale: { + type: ScaleTypes.LINEAR, + mode: AxisModes.NORMAL, + }, + labels: { show: true, - type: ChartTypes.HISTOGRAM, - mode: ChartModes.STACKED, - data: { - label: countLabel, - id: '1' - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true + rotate: Rotates.HORIZONTAL, + filter: false, + truncate: 100 + }, + title: { + text: countLabel, } - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: { - show: false, - }, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: palettes.euiPaletteColorBlind.colors[9] } - }, - }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ + ], + seriesParams: [ { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('kbnVislibVisTypes.histogram.metricTitle', { defaultMessage: 'Y-axis' }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [ - { schema: 'metric', type: 'count' } - ] - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('kbnVislibVisTypes.histogram.radiusTitle', { defaultMessage: 'Dot size' }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('kbnVislibVisTypes.histogram.segmentTitle', { defaultMessage: 'X-axis' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('kbnVislibVisTypes.histogram.groupTitle', { defaultMessage: 'Split series' }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('kbnVislibVisTypes.histogram.splitTitle', { defaultMessage: 'Split chart' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + show: true, + type: ChartTypes.HISTOGRAM, + mode: ChartModes.STACKED, + data: { + label: countLabel, + id: '1' + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true } - ]) - } + ], + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + times: [], + addTimeMarker: false, + labels: { + show: false, + }, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: ThresholdLineStyles.FULL, + color: palettes.euiPaletteColorBlind.colors[9] + } + }, + }, + events: { + brush: { disabled: false }, + }, + editorConfig: { + collections: getConfigCollections(), + optionTabs: getAreaOptionTabs(), + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('kbnVislibVisTypes.histogram.metricTitle', { defaultMessage: 'Y-axis' }), + min: 1, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: AggGroupNames.Metrics, + name: 'radius', + title: i18n.translate('kbnVislibVisTypes.histogram.radiusTitle', { defaultMessage: 'Dot size' }), + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('kbnVislibVisTypes.histogram.segmentTitle', { defaultMessage: 'X-axis' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('kbnVislibVisTypes.histogram.groupTitle', { defaultMessage: 'Split series' }), + min: 0, + max: 3, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('kbnVislibVisTypes.histogram.splitTitle', { defaultMessage: 'Split chart' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + } + ]) + } - }); -} +}; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js index 2f8107580f0f7..0369e8d8c27b5 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/horizontal_bar.js @@ -17,7 +17,6 @@ * under the License. */ -import { visFactory } from '../../../ui/public/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggGroupNames } from 'ui/vis/editors/default'; @@ -36,144 +35,141 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { palettes } from '@elastic/eui/lib/services'; import { vislibVisController } from './controller'; -export default function PointSeriesVisType() { - - return visFactory.createBaseVisualization({ - name: 'horizontal_bar', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.horizontalBarTitle', { defaultMessage: 'Horizontal Bar' }), - icon: 'visBarHorizontal', - description: i18n.translate('kbnVislibVisTypes.horizontalBar.horizontalBarDescription', - { defaultMessage: 'Assign a continuous variable to each axis' } - ), - visualization: vislibVisController, - visConfig: { - defaults: { - type: 'histogram', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.LEFT, - show: true, - style: { - }, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 200 - }, - title: {} - } - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.BOTTOM, +export const horizontalBarDefinition = { + name: 'horizontal_bar', + title: i18n.translate('kbnVislibVisTypes.horizontalBar.horizontalBarTitle', { defaultMessage: 'Horizontal Bar' }), + icon: 'visBarHorizontal', + description: i18n.translate('kbnVislibVisTypes.horizontalBar.horizontalBarDescription', + { defaultMessage: 'Assign a continuous variable to each axis' } + ), + visualization: vislibVisController, + visConfig: { + defaults: { + type: 'histogram', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: AxisTypes.CATEGORY, + position: Positions.LEFT, + show: true, + style: { + }, + scale: { + type: ScaleTypes.LINEAR, + }, + labels: { show: true, - style: { - }, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.ANGLED, - filter: true, - truncate: 100 - }, - title: { - text: countLabel, - } - } - ], - seriesParams: [{ + rotate: Rotates.HORIZONTAL, + filter: false, + truncate: 200 + }, + title: {} + } + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: AxisTypes.VALUE, + position: Positions.BOTTOM, show: true, - type: ChartTypes.HISTOGRAM, - mode: ChartModes.NORMAL, - data: { - label: countLabel, - id: '1' + style: { }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true - }], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: palettes.euiPaletteColorBlind.colors[9] + scale: { + type: ScaleTypes.LINEAR, + mode: AxisModes.NORMAL, + }, + labels: { + show: true, + rotate: Rotates.ANGLED, + filter: true, + truncate: 100 + }, + title: { + text: countLabel, + } + } + ], + seriesParams: [{ + show: true, + type: ChartTypes.HISTOGRAM, + mode: ChartModes.NORMAL, + data: { + label: countLabel, + id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true + }], + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: ThresholdLineStyles.FULL, + color: palettes.euiPaletteColorBlind.colors[9] }, }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.metricTitle', { defaultMessage: 'Y-axis' }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [ - { schema: 'metric', type: 'count' } - ] - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.radiusTitle', { defaultMessage: 'Dot size' }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.segmentTitle', { defaultMessage: 'X-axis' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.groupTitle', { defaultMessage: 'Split series' }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('kbnVislibVisTypes.horizontalBar.splitTitle', { defaultMessage: 'Split chart' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - } - ]) - } - }); -} + }, + events: { + brush: { disabled: false }, + }, + editorConfig: { + collections: getConfigCollections(), + optionTabs: getAreaOptionTabs(), + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('kbnVislibVisTypes.horizontalBar.metricTitle', { defaultMessage: 'Y-axis' }), + min: 1, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: AggGroupNames.Metrics, + name: 'radius', + title: i18n.translate('kbnVislibVisTypes.horizontalBar.radiusTitle', { defaultMessage: 'Dot size' }), + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'] + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('kbnVislibVisTypes.horizontalBar.segmentTitle', { defaultMessage: 'X-axis' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('kbnVislibVisTypes.horizontalBar.groupTitle', { defaultMessage: 'Split series' }), + min: 0, + max: 3, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('kbnVislibVisTypes.horizontalBar.splitTitle', { defaultMessage: 'Split chart' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + } + ]) + } +}; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js index fe2cca3b80064..c82073ff582b8 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js @@ -19,20 +19,20 @@ import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy'; -import histogramVisTypeProvider from './histogram'; -import lineVisTypeProvider from './line'; -import pieVisTypeProvider from './pie'; -import areaVisTypeProvider from './area'; -import heatmapVisTypeProvider from './heatmap'; -import horizontalBarVisTypeProvider from './horizontal_bar'; -import gaugeVisTypeProvider from './gauge'; -import goalVisTypeProvider from './goal'; +import { histogramDefinition } from './histogram'; +import { lineDefinition } from './line'; +import { pieDefinition } from './pie'; +import { areaDefinition } from './area'; +import { heatmapDefinition } from './heatmap'; +import { horizontalBarDefinition } from './horizontal_bar'; +import { gaugeDefinition } from './gauge'; +import { goalDefinition } from './goal'; -visualizations.types.registerVisualization(histogramVisTypeProvider); -visualizations.types.registerVisualization(lineVisTypeProvider); -visualizations.types.registerVisualization(pieVisTypeProvider); -visualizations.types.registerVisualization(areaVisTypeProvider); -visualizations.types.registerVisualization(heatmapVisTypeProvider); -visualizations.types.registerVisualization(horizontalBarVisTypeProvider); -visualizations.types.registerVisualization(gaugeVisTypeProvider); -visualizations.types.registerVisualization(goalVisTypeProvider); +visualizations.types.createBaseVisualization(histogramDefinition); +visualizations.types.createBaseVisualization(lineDefinition); +visualizations.types.createBaseVisualization(pieDefinition); +visualizations.types.createBaseVisualization(areaDefinition); +visualizations.types.createBaseVisualization(heatmapDefinition); +visualizations.types.createBaseVisualization(horizontalBarDefinition); +visualizations.types.createBaseVisualization(gaugeDefinition); +visualizations.types.createBaseVisualization(goalDefinition); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js index fedbf48541451..74c7e2fa2af89 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js @@ -17,7 +17,6 @@ * under the License. */ -import { visFactory } from '../../../ui/public/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggGroupNames } from 'ui/vis/editors/default'; @@ -37,142 +36,139 @@ import { palettes } from '@elastic/eui/lib/services'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { vislibVisController } from './controller'; -export default function PointSeriesVisType() { - - return visFactory.createBaseVisualization({ - name: 'line', - title: i18n.translate('kbnVislibVisTypes.line.lineTitle', { defaultMessage: 'Line' }), - icon: 'visLine', - description: i18n.translate('kbnVislibVisTypes.line.lineDescription', { defaultMessage: 'Emphasize trends' }), - visualization: vislibVisController, - visConfig: { - defaults: { - type: 'line', - grid: { - categoryLines: false, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - }, - labels: { - show: true, - filter: true, - truncate: 100 - }, - title: {} - } - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, +export const lineDefinition = { + name: 'line', + title: i18n.translate('kbnVislibVisTypes.line.lineTitle', { defaultMessage: 'Line' }), + icon: 'visLine', + description: i18n.translate('kbnVislibVisTypes.line.lineDescription', { defaultMessage: 'Emphasize trends' }), + visualization: vislibVisController, + visConfig: { + defaults: { + type: 'line', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: AxisTypes.CATEGORY, + position: Positions.BOTTOM, + show: true, + style: {}, + scale: { + type: ScaleTypes.LINEAR, + }, + labels: { show: true, - style: {}, - scale: { - type: ScaleTypes.LINEAR, - mode: AxisModes.NORMAL, - }, - labels: { - show: true, - rotate: Rotates.HORIZONTAL, - filter: false, - truncate: 100 - }, - title: { - text: countLabel, - } - } - ], - seriesParams: [ - { + filter: true, + truncate: 100 + }, + title: {} + } + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: AxisTypes.VALUE, + position: Positions.LEFT, + show: true, + style: {}, + scale: { + type: ScaleTypes.LINEAR, + mode: AxisModes.NORMAL, + }, + labels: { show: true, - type: ChartTypes.LINE, - mode: ChartModes.NORMAL, - data: { - label: countLabel, - id: '1' - }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - interpolate: InterpolationModes.LINEAR, - showCircles: true + rotate: Rotates.HORIZONTAL, + filter: false, + truncate: 100 + }, + title: { + text: countLabel, } - ], - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: ThresholdLineStyles.FULL, - color: palettes.euiPaletteColorBlind.colors[9] } - }, - }, - events: { - brush: { disabled: false }, - }, - editorConfig: { - collections: getConfigCollections(), - optionTabs: getAreaOptionTabs(), - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('kbnVislibVisTypes.line.metricTitle', { defaultMessage: 'Y-axis' }), - min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds'], - defaults: [ - { schema: 'metric', type: 'count' } - ] - }, - { - group: AggGroupNames.Metrics, - name: 'radius', - title: i18n.translate('kbnVislibVisTypes.line.radiusTitle', { defaultMessage: 'Dot size' }), - min: 0, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'] - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('kbnVislibVisTypes.line.segmentTitle', { defaultMessage: 'X-axis' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, - { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('kbnVislibVisTypes.line.groupTitle', { defaultMessage: 'Split series' }), - min: 0, - max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, + ], + seriesParams: [ { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('kbnVislibVisTypes.line.splitTitle', { defaultMessage: 'Split chart' }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + show: true, + type: ChartTypes.LINE, + mode: ChartModes.NORMAL, + data: { + label: countLabel, + id: '1' + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + interpolate: InterpolationModes.LINEAR, + showCircles: true } - ]) - } - }); -} + ], + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: ThresholdLineStyles.FULL, + color: palettes.euiPaletteColorBlind.colors[9] + } + }, + }, + events: { + brush: { disabled: false }, + }, + editorConfig: { + collections: getConfigCollections(), + optionTabs: getAreaOptionTabs(), + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('kbnVislibVisTypes.line.metricTitle', { defaultMessage: 'Y-axis' }), + min: 1, + aggFilter: ['!geo_centroid', '!geo_bounds'], + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: AggGroupNames.Metrics, + name: 'radius', + title: i18n.translate('kbnVislibVisTypes.line.radiusTitle', { defaultMessage: 'Dot size' }), + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'] + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('kbnVislibVisTypes.line.segmentTitle', { defaultMessage: 'X-axis' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('kbnVislibVisTypes.line.groupTitle', { defaultMessage: 'Split series' }), + min: 0, + max: 3, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('kbnVislibVisTypes.line.splitTitle', { defaultMessage: 'Split chart' }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + } + ]) + } +}; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js index 8a374f21dcb09..691f2e6349f72 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/pie.js @@ -17,7 +17,6 @@ * under the License. */ -import { visFactory } from '../../../ui/public/vis/vis_factory'; import { i18n } from '@kbn/i18n'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { AggGroupNames } from 'ui/vis/editors/default'; @@ -25,66 +24,63 @@ import { PieOptions } from './components/options'; import { getPositions, Positions } from './utils/collections'; import { vislibVisController } from './controller'; -export default function HistogramVisType() { - - return visFactory.createBaseVisualization({ - name: 'pie', - title: i18n.translate('kbnVislibVisTypes.pie.pieTitle', { defaultMessage: 'Pie' }), - icon: 'visPie', - description: i18n.translate('kbnVislibVisTypes.pie.pieDescription', { defaultMessage: 'Compare parts of a whole' }), - visualization: vislibVisController, - visConfig: { - defaults: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: Positions.RIGHT, - isDonut: true, - labels: { - show: false, - values: true, - last_level: true, - truncate: 100 - } - }, +export const pieDefinition = { + name: 'pie', + title: i18n.translate('kbnVislibVisTypes.pie.pieTitle', { defaultMessage: 'Pie' }), + icon: 'visPie', + description: i18n.translate('kbnVislibVisTypes.pie.pieDescription', { defaultMessage: 'Compare parts of a whole' }), + visualization: vislibVisController, + visConfig: { + defaults: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: Positions.RIGHT, + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100 + } }, - editorConfig: { - collections: { - legendPositions: getPositions() - }, - optionsTemplate: PieOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('kbnVislibVisTypes.pie.metricTitle', { defaultMessage: 'Slice size' }), - min: 1, - max: 1, - aggFilter: ['sum', 'count', 'cardinality', 'top_hits'], - defaults: [ - { schema: 'metric', type: 'count' } - ] - }, - { - group: AggGroupNames.Buckets, - name: 'segment', - title: i18n.translate('kbnVislibVisTypes.pie.segmentTitle', { defaultMessage: 'Split slices' }), - min: 0, - max: Infinity, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('kbnVislibVisTypes.pie.splitTitle', { defaultMessage: 'Split chart' }), - mustBeFirst: true, - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] - } - ]) + }, + editorConfig: { + collections: { + legendPositions: getPositions() }, - hierarchicalData: true, - responseHandler: 'vislib_slices', - }); -} + optionsTemplate: PieOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('kbnVislibVisTypes.pie.metricTitle', { defaultMessage: 'Slice size' }), + min: 1, + max: 1, + aggFilter: ['sum', 'count', 'cardinality', 'top_hits'], + defaults: [ + { schema: 'metric', type: 'count' } + ] + }, + { + group: AggGroupNames.Buckets, + name: 'segment', + title: i18n.translate('kbnVislibVisTypes.pie.segmentTitle', { defaultMessage: 'Split slices' }), + min: 0, + max: Infinity, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('kbnVislibVisTypes.pie.splitTitle', { defaultMessage: 'Split chart' }), + mustBeFirst: true, + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'] + } + ]) + }, + hierarchicalData: true, + responseHandler: 'vislib_slices', +}; diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index fc5f34fab7564..5a10e02ba8131 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -81,7 +81,7 @@ export function getServices() { // EXPORT legacy static dependencies export { angular }; -export { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/build_pipeline'; +export { buildVislibDimensions } from '../../../visualizations/public'; // @ts-ignore export { callAfterBindingsWorkaround } from 'ui/compat'; export { diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index aec80b8d13551..ae4b4d1c779df 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -27,7 +27,7 @@ import { Vis } from 'ui/vis'; import { uiModules } from 'ui/modules'; -import { updateOldState } from 'ui/vis/vis_update_state'; +import { updateOldState } from '../../../../visualizations/public'; import { VisualizeConstants } from '../visualize_constants'; import { createLegacyClass } from 'ui/utils/legacy_class'; import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/type_selection/type_selection.tsx b/src/legacy/core_plugins/kibana/public/visualize/wizard/type_selection/type_selection.tsx index 245b4270c6aea..fcc612ab49bd2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/type_selection/type_selection.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/type_selection/type_selection.tsx @@ -34,7 +34,7 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { memoizeLast } from 'ui/utils/memoize'; +import { memoizeLast } from '../../../../../visualizations/public/np_ready/public/legacy/memoize'; import { VisType } from '../../kibana_services'; import { VisTypeAlias } from '../../../../../visualizations/public'; import { NewVisHelp } from './new_vis_help'; diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js index b57fbd637f0b7..7571c616093ba 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -109,7 +109,7 @@ describe('RegionMapsVisualizationTests', function () { if(!visRegComplete) { visRegComplete = true; - visualizationsSetup.types.registerVisualization(() => createRegionMapTypeDefinition(dependencies)); + visualizationsSetup.types.createBaseVisualization(createRegionMapTypeDefinition(dependencies)); } RegionMapsVisualization = createRegionMapVisualization(dependencies); diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/legacy/core_plugins/region_map/public/plugin.ts index aaaf4c866c521..a41d638986ae5 100644 --- a/src/legacy/core_plugins/region_map/public/plugin.ts +++ b/src/legacy/core_plugins/region_map/public/plugin.ts @@ -70,7 +70,7 @@ export class RegionMapPlugin implements Plugin, void> { expressions.registerFunction(createRegionMapFn); - visualizations.types.registerVisualization(() => + visualizations.types.createBaseVisualization( createRegionMapTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/region_map/public/region_map_type.js b/src/legacy/core_plugins/region_map/public/region_map_type.js index 3a28277f9f4c7..03e0de728ca85 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_type.js +++ b/src/legacy/core_plugins/region_map/public/region_map_type.js @@ -22,11 +22,9 @@ import { Schemas } from 'ui/vis/editors/default/schemas'; import { colorSchemas } from 'ui/vislib/components/color/truncated_colormaps'; import { mapToLayerWithId } from './util'; import { createRegionMapVisualization } from './region_map_visualization'; -import { Status } from 'ui/vis/update_status'; +import { Status } from '../../visualizations/public'; import { RegionMapOptions } from './components/region_map_options'; -import { visFactory } from '../../visualizations/public'; - // TODO: reference to TILE_MAP plugin should be removed import { ORIGIN } from '../../tile_map/common/origin'; @@ -34,7 +32,7 @@ export function createRegionMapTypeDefinition(dependencies) { const { uiSettings, regionmapsConfig, serviceSettings } = dependencies; const visualization = createRegionMapVisualization(dependencies); - return visFactory.createBaseVisualization({ + return { name: 'region_map', title: i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }), description: i18n.translate('regionMap.mapVis.regionMapDescription', { @@ -155,5 +153,5 @@ provided base maps, or add your own. Darker colors represent higher values.', return savedVis; }, - }); + }; } diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 0e3c4fdd9d355..57469625ea4a6 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -86,7 +86,7 @@ describe('CoordinateMapsVisualizationTest', function () { if(!visRegComplete) { visRegComplete = true; - visualizationsSetup.types.registerVisualization(() => createTileMapTypeDefinition(dependencies)); + visualizationsSetup.types.createBaseVisualization(createTileMapTypeDefinition(dependencies)); } diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/legacy/core_plugins/tile_map/public/plugin.ts index 602f6c266b5f6..14a348f624002 100644 --- a/src/legacy/core_plugins/tile_map/public/plugin.ts +++ b/src/legacy/core_plugins/tile_map/public/plugin.ts @@ -64,7 +64,7 @@ export class TileMapPlugin implements Plugin, void> { expressions.registerFunction(() => createTileMapFn(visualizationDependencies)); - visualizations.types.registerVisualization(() => + visualizations.types.createBaseVisualization( createTileMapTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index 243b4c2bf7765..a976fb5c77ef0 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -22,12 +22,11 @@ import { i18n } from '@kbn/i18n'; import { supports } from 'ui/utils/supports'; import { Schemas } from 'ui/vis/editors/default/schemas'; -import { Status } from 'ui/vis/update_status'; import { colorSchemas } from 'ui/vislib/components/color/truncated_colormaps'; import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; import { createTileMapVisualization } from './tile_map_visualization'; -import { visFactory } from '../../visualizations/public'; +import { Status } from '../../visualizations/public'; import { TileMapOptions } from './components/tile_map_options'; import { MapTypes } from './map_types'; @@ -35,7 +34,7 @@ export function createTileMapTypeDefinition(dependencies) { const CoordinateMapsVisualization = createTileMapVisualization(dependencies); const { uiSettings, serviceSettings } = dependencies; - return visFactory.createBaseVisualization({ + return { name: 'tile_map', title: i18n.translate('tileMap.vis.mapTitle', { defaultMessage: 'Coordinate Map', @@ -160,5 +159,5 @@ export function createTileMapTypeDefinition(dependencies) { } return savedVis; }, - }); + }; } diff --git a/src/legacy/core_plugins/timelion/public/plugin.ts b/src/legacy/core_plugins/timelion/public/plugin.ts index 6291948f75077..b0123cd34b49e 100644 --- a/src/legacy/core_plugins/timelion/public/plugin.ts +++ b/src/legacy/core_plugins/timelion/public/plugin.ts @@ -76,7 +76,7 @@ export class TimelionPlugin implements Plugin, void> { this.registerPanels(dependencies); expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies)); - visualizations.types.registerVisualization(() => getTimelionVisualization(dependencies)); + visualizations.types.createBaseVisualization(getTimelionVisualization(dependencies)); } private registerPanels(dependencies: TimelionVisualizationDependencies) { diff --git a/src/legacy/core_plugins/timelion/public/vis/index.ts b/src/legacy/core_plugins/timelion/public/vis/index.ts index a586fd71e6cf8..7b82553a24e5b 100644 --- a/src/legacy/core_plugins/timelion/public/vis/index.ts +++ b/src/legacy/core_plugins/timelion/public/vis/index.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { DefaultEditorSize } from 'ui/vis/editor_size'; -import { visFactory } from '../../../visualizations/public'; import { getTimelionRequestHandler } from './timelion_request_handler'; import visConfigTemplate from './timelion_vis.html'; import editorConfigTemplate from './timelion_vis_params.html'; @@ -35,7 +34,7 @@ export function getTimelionVisualization(dependencies: TimelionVisualizationDepe // return the visType object, which kibana will use to display and configure new // Vis object of this type. - return visFactory.createBaseVisualization({ + return { name: TIMELION_VIS_NAME, title: 'Timelion', icon: 'visTimelion', @@ -61,5 +60,5 @@ export function getTimelionVisualization(dependencies: TimelionVisualizationDepe showQueryBar: false, showFilterBar: false, }, - }); + }; } diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts index 7b2f8f6c236b2..524bbeed1b552 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts @@ -19,13 +19,13 @@ import { i18n } from '@kbn/i18n'; -import { visFactory, DefaultEditorSize } from '../../visualizations/public'; +import { DefaultEditorSize } from '../../visualizations/public'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; import { SettingsOptions } from './settings_options'; -export const markdownVis = visFactory.createReactVisualization({ +export const markdownVisDefinition = { name: 'markdown', title: 'Markdown', isAccessible: true, @@ -67,4 +67,4 @@ export const markdownVis = visFactory.createReactVisualization({ }, requestHandler: 'none', responseHandler: 'none', -}); +}; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts index 85d8c27ed970d..f131664756202 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts @@ -21,7 +21,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../.. import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; -import { markdownVis } from './markdown_vis'; +import { markdownVisDefinition } from './markdown_vis'; import { createMarkdownVisFn } from './markdown_fn'; /** @internal */ @@ -39,7 +39,7 @@ export class MarkdownPlugin implements Plugin { } public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) { - visualizations.types.registerVisualization(() => markdownVis); + visualizations.types.createReactVisualization(markdownVisDefinition); expressions.registerFunction(createMarkdownVisFn); } diff --git a/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis.js b/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis.js index 384beb3764e2e..de126087b36be 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis.js +++ b/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis.js @@ -24,7 +24,7 @@ import expect from '@kbn/expect'; import { Vis } from 'ui/vis'; import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { createMetricVisTypeDefinition } from '../metric_vis_type'; +import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; describe('metric_vis - createMetricVisTypeDefinition', () => { let setup = null; @@ -34,7 +34,7 @@ describe('metric_vis - createMetricVisTypeDefinition', () => { beforeEach( ngMock.inject(Private => { setup = () => { - const metricVisType = createMetricVisTypeDefinition(); + const metricVisType = visualizations.types.get('metric'); const indexPattern = Private(LogstashIndexPatternStubProvider); indexPattern.stubSetFieldFormat('ip', 'url', { diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts index a05df6f4d1564..ceab5dafe1f06 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts @@ -28,107 +28,104 @@ import { colorSchemas, ColorSchemas } from 'ui/vislib/components/color/colormaps // @ts-ignore import { MetricVisComponent } from './components/metric_vis_controller'; -import { visFactory } from '../../visualizations/public'; import { MetricVisOptions } from './components/metric_vis_options'; import { ColorModes } from '../../kbn_vislib_vis_types/public/utils/collections'; -export const createMetricVisTypeDefinition = () => { - return visFactory.createReactVisualization({ - name: 'metric', - title: i18n.translate('visTypeMetric.metricTitle', { defaultMessage: 'Metric' }), - icon: 'visMetric', - description: i18n.translate('visTypeMetric.metricDescription', { - defaultMessage: 'Display a calculation as a single number', - }), - visConfig: { - component: MetricVisComponent, - defaults: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: ColorSchemas.GreenToRed, - metricColorMode: ColorModes.NONE, - colorsRange: [{ from: 0, to: 10000 }], - labels: { - show: true, - }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 60, - }, +export const metricVisDefinition = { + name: 'metric', + title: i18n.translate('visTypeMetric.metricTitle', { defaultMessage: 'Metric' }), + icon: 'visMetric', + description: i18n.translate('visTypeMetric.metricDescription', { + defaultMessage: 'Display a calculation as a single number', + }), + visConfig: { + component: MetricVisComponent, + defaults: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: ColorSchemas.GreenToRed, + metricColorMode: ColorModes.NONE, + colorsRange: [{ from: 0, to: 10000 }], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 60, }, }, }, - editorConfig: { - collections: { - metricColorMode: [ - { - id: ColorModes.NONE, - label: i18n.translate('visTypeMetric.colorModes.noneOptionLabel', { - defaultMessage: 'None', - }), - }, - { - id: ColorModes.LABELS, - label: i18n.translate('visTypeMetric.colorModes.labelsOptionLabel', { - defaultMessage: 'Labels', - }), - }, - { - id: ColorModes.BACKGROUND, - label: i18n.translate('visTypeMetric.colorModes.backgroundOptionLabel', { - defaultMessage: 'Background', - }), - }, - ], - colorSchemas, - }, - optionsTemplate: MetricVisOptions, - schemas: new Schemas([ + }, + editorConfig: { + collections: { + metricColorMode: [ { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeMetric.schemas.metricTitle', { defaultMessage: 'Metric' }), - min: 1, - aggFilter: [ - '!std_dev', - '!geo_centroid', - '!derivative', - '!serial_diff', - '!moving_avg', - '!cumulative_sum', - '!geo_bounds', - ], - aggSettings: { - top_hits: { - allowStrings: true, - }, - }, - defaults: [ - { - type: 'count', - schema: 'metric', - }, - ], + id: ColorModes.NONE, + label: i18n.translate('visTypeMetric.colorModes.noneOptionLabel', { + defaultMessage: 'None', + }), + }, + { + id: ColorModes.LABELS, + label: i18n.translate('visTypeMetric.colorModes.labelsOptionLabel', { + defaultMessage: 'Labels', + }), }, { - group: AggGroupNames.Buckets, - name: 'group', - title: i18n.translate('visTypeMetric.schemas.splitGroupTitle', { - defaultMessage: 'Split group', + id: ColorModes.BACKGROUND, + label: i18n.translate('visTypeMetric.colorModes.backgroundOptionLabel', { + defaultMessage: 'Background', }), - min: 0, - max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], }, - ]), + ], + colorSchemas, }, - }); + optionsTemplate: MetricVisOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeMetric.schemas.metricTitle', { defaultMessage: 'Metric' }), + min: 1, + aggFilter: [ + '!std_dev', + '!geo_centroid', + '!derivative', + '!serial_diff', + '!moving_avg', + '!cumulative_sum', + '!geo_bounds', + ], + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, + defaults: [ + { + type: 'count', + schema: 'metric', + }, + ], + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('visTypeMetric.schemas.splitGroupTitle', { + defaultMessage: 'Split group', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + }, + ]), + }, }; diff --git a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts index e3e0ffaab2116..f5c152ce888c0 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts @@ -22,7 +22,7 @@ import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressio import { VisualizationsSetup } from '../../visualizations/public'; import { createMetricVisFn } from './metric_vis_fn'; -import { createMetricVisTypeDefinition } from './metric_vis_type'; +import { metricVisDefinition } from './metric_vis_type'; /** @internal */ export interface MetricVisPluginSetupDependencies { @@ -40,7 +40,7 @@ export class MetricVisPlugin implements Plugin { public setup(core: CoreSetup, { expressions, visualizations }: MetricVisPluginSetupDependencies) { expressions.registerFunction(createMetricVisFn); - visualizations.types.registerVisualization(createMetricVisTypeDefinition); + visualizations.types.createReactVisualization(metricVisDefinition); } public start(core: CoreStart) { diff --git a/src/legacy/core_plugins/vis_type_metric/public/types.ts b/src/legacy/core_plugins/vis_type_metric/public/types.ts index 54f1f36e19c99..ce0e78140a86a 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/types.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/types.ts @@ -19,7 +19,7 @@ import { ColorSchemas } from 'ui/vislib/components/color/colormaps'; import { RangeValues } from 'ui/vis/editors/default/controls/ranges'; -import { SchemaConfig } from 'ui/visualize/loader/pipeline_helpers/build_pipeline'; +import { SchemaConfig } from '../../visualizations/public'; import { ColorModes } from '../../kbn_vislib_vis_types/public/utils/collections'; import { Labels, Style } from '../../kbn_vislib_vis_types/public/types'; diff --git a/src/legacy/core_plugins/vis_type_table/public/__tests__/table_vis_controller.js b/src/legacy/core_plugins/vis_type_table/public/__tests__/table_vis_controller.js index 4153ce2da36a7..e22dd4caa6d01 100644 --- a/src/legacy/core_plugins/vis_type_table/public/__tests__/table_vis_controller.js +++ b/src/legacy/core_plugins/vis_type_table/public/__tests__/table_vis_controller.js @@ -21,13 +21,12 @@ import $ from 'jquery'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; -import { Vis } from 'ui/vis'; -import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { Vis } from '../../../visualizations/public'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { AppStateProvider } from 'ui/state_management/app_state'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; -import { createTableVisTypeDefinition } from '../table_vis_type'; +import { tableVisTypeDefinition } from '../table_vis_type'; import { setup as visualizationsSetup } from '../../../visualizations/public/np_ready/public/legacy'; describe('Table Vis - Controller', async function () { @@ -40,18 +39,10 @@ describe('Table Vis - Controller', async function () { let AppState; let tableAggResponse; let tabifiedResponse; - let legacyDependencies; - ngMock.inject(function ($injector) { - Private = $injector.get('Private'); - legacyDependencies = { - // eslint-disable-next-line new-cap - createAngularVisualization: VisFactoryProvider(Private).createAngularVisualization, - }; + ngMock.inject(function () { - visualizationsSetup.types.registerVisualization(() => - createTableVisTypeDefinition(legacyDependencies) - ); + visualizationsSetup.types.createBaseVisualization(tableVisTypeDefinition); }); beforeEach(ngMock.module('kibana', 'kibana/table_vis')); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js index 13e8a4fd9535a..2978856a3511d 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js @@ -25,12 +25,11 @@ import fixtures from 'fixtures/fake_hierarchical_data'; import sinon from 'sinon'; import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { Vis } from 'ui/vis'; +import { Vis } from '../../../../visualizations/public'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { round } from 'lodash'; -import { VisFactoryProvider } from 'ui/vis/vis_factory'; -import { createTableVisTypeDefinition } from '../../table_vis_type'; +import { tableVisTypeDefinition } from '../../table_vis_type'; import { setup as visualizationsSetup } from '../../../../visualizations/public/np_ready/public/legacy'; describe('Table Vis - AggTable Directive', function () { @@ -39,7 +38,6 @@ describe('Table Vis - AggTable Directive', function () { let indexPattern; let settings; let tableAggResponse; - let legacyDependencies; const tabifiedData = {}; const init = () => { @@ -98,13 +96,8 @@ describe('Table Vis - AggTable Directive', function () { ); }; - ngMock.inject(function (Private) { - legacyDependencies = { - // eslint-disable-next-line new-cap - createAngularVisualization: VisFactoryProvider(Private).createAngularVisualization, - }; - - visualizationsSetup.types.registerVisualization(() => createTableVisTypeDefinition(legacyDependencies)); + ngMock.inject(function () { + visualizationsSetup.types.createBaseVisualization(tableVisTypeDefinition); }); beforeEach(ngMock.module('kibana')); diff --git a/src/legacy/core_plugins/vis_type_table/public/plugin.ts b/src/legacy/core_plugins/vis_type_table/public/plugin.ts index 28e812701c2d3..ce8d349d8dd7a 100644 --- a/src/legacy/core_plugins/vis_type_table/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_table/public/plugin.ts @@ -24,7 +24,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../.. import { LegacyDependenciesPlugin } from './shim'; import { createTableVisFn } from './table_vis_fn'; -import { createTableVisTypeDefinition } from './table_vis_type'; +import { tableVisTypeDefinition } from './table_vis_type'; /** @internal */ export interface TablePluginSetupDependencies { @@ -48,7 +48,7 @@ export class TableVisPlugin implements Plugin, void> { __LEGACY.setup(); expressions.registerFunction(createTableVisFn); - visualizations.types.registerVisualization(createTableVisTypeDefinition); + visualizations.types.createBaseVisualization(tableVisTypeDefinition); } public start(core: CoreStart) { diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts index 1de72c0b33a4c..7e8537a1fee54 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { Vis } from 'ui/vis'; // @ts-ignore -import { visFactory } from 'ui/vis/vis_factory'; // @ts-ignore import { Schemas } from 'ui/vis/editors/default/schemas'; @@ -32,74 +31,72 @@ import { tableVisResponseHandler } from './table_vis_request_handler'; import tableVisTemplate from './table_vis.html'; import { TableOptions } from './components/table_vis_options'; -export const createTableVisTypeDefinition = () => { - return visFactory.createBaseVisualization({ - type: 'table', - name: 'table', - title: i18n.translate('visTypeTable.tableVisTitle', { - defaultMessage: 'Data Table', - }), - icon: 'visTable', - description: i18n.translate('visTypeTable.tableVisDescription', { - defaultMessage: 'Display values in a table', - }), - visualization: AngularVisController, - visConfig: { - defaults: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', +export const tableVisTypeDefinition = { + type: 'table', + name: 'table', + title: i18n.translate('visTypeTable.tableVisTitle', { + defaultMessage: 'Data Table', + }), + icon: 'visTable', + description: i18n.translate('visTypeTable.tableVisDescription', { + defaultMessage: 'Display values in a table', + }), + visualization: AngularVisController, + visConfig: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, }, - template: tableVisTemplate, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', }, - editorConfig: { - optionsTemplate: TableOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { - defaultMessage: 'Metric', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - aggSettings: { - top_hits: { - allowStrings: true, - }, + template: tableVisTemplate, + }, + editorConfig: { + optionsTemplate: TableOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, }, - min: 1, - defaults: [{ type: 'count', schema: 'metric' }], - }, - { - group: AggGroupNames.Buckets, - name: 'bucket', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { - defaultMessage: 'Split rows', - }), - aggFilter: ['!filter'], }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { - defaultMessage: 'Split table', - }), - min: 0, - max: 1, - aggFilter: ['!filter'], - }, - ]), - }, - responseHandler: tableVisResponseHandler, - hierarchicalData: (vis: Vis) => { - return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); - }, - }); + min: 1, + defaults: [{ type: 'count', schema: 'metric' }], + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + }, + ]), + }, + responseHandler: tableVisResponseHandler, + hierarchicalData: (vis: Vis) => { + return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + }, }; diff --git a/src/legacy/core_plugins/vis_type_table/public/types.ts b/src/legacy/core_plugins/vis_type_table/public/types.ts index 4a16bb72bd2e3..39023d1305cb6 100644 --- a/src/legacy/core_plugins/vis_type_table/public/types.ts +++ b/src/legacy/core_plugins/vis_type_table/public/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SchemaConfig } from 'ui/visualize/loader/pipeline_helpers/build_pipeline'; +import { SchemaConfig } from '../../visualizations/public'; export enum AggTypes { SUM = 'sum', diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts index bcb210e9ed081..865229ce0e4c1 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts @@ -22,7 +22,7 @@ import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressio import { VisualizationsSetup } from '../../visualizations/public'; import { createTagCloudFn } from './tag_cloud_fn'; -import { createTagCloudTypeDefinition } from './tag_cloud_type'; +import { tagcloudVisDefinition } from './tag_cloud_type'; /** @internal */ export interface TagCloudPluginSetupDependencies { @@ -40,7 +40,7 @@ export class TagCloudPlugin implements Plugin { public setup(core: CoreSetup, { expressions, visualizations }: TagCloudPluginSetupDependencies) { expressions.registerFunction(createTagCloudFn); - visualizations.types.registerVisualization(createTagCloudTypeDefinition); + visualizations.types.createBaseVisualization(tagcloudVisDefinition); } public start(core: CoreStart) { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 0b4d90522cc44..9c673b1122573 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -18,110 +18,107 @@ */ import { i18n } from '@kbn/i18n'; -import { Status } from 'ui/vis/update_status'; // @ts-ignore import { Schemas } from 'ui/vis/editors/default/schemas'; +import { Status } from '../../visualizations/public'; import { TagCloudOptions } from './components/tag_cloud_options'; -import { visFactory } from '../../visualizations/public'; // @ts-ignore import { TagCloudVisualization } from './components/tag_cloud_visualization'; -export const createTagCloudTypeDefinition = () => { - return visFactory.createBaseVisualization({ - name: 'tagcloud', - title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }), - icon: 'visTagCloud', - description: i18n.translate('visTypeTagCloud.vis.tagCloudDescription', { - defaultMessage: 'A group of words, sized according to their importance', - }), - visConfig: { - defaults: { - scale: 'linear', - orientation: 'single', - minFontSize: 18, - maxFontSize: 72, - showLabel: true, - }, +export const tagcloudVisDefinition = { + name: 'tagcloud', + title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }), + icon: 'visTagCloud', + description: i18n.translate('visTypeTagCloud.vis.tagCloudDescription', { + defaultMessage: 'A group of words, sized according to their importance', + }), + visConfig: { + defaults: { + scale: 'linear', + orientation: 'single', + minFontSize: 18, + maxFontSize: 72, + showLabel: true, }, - requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.DATA], - visualization: TagCloudVisualization, - editorConfig: { - collections: { - scales: [ - { - text: i18n.translate('visTypeTagCloud.vis.editorConfig.scales.linearText', { - defaultMessage: 'Linear', - }), - value: 'linear', - }, - { - text: i18n.translate('visTypeTagCloud.vis.editorConfig.scales.logText', { - defaultMessage: 'Log', - }), - value: 'log', - }, - { - text: i18n.translate('visTypeTagCloud.vis.editorConfig.scales.squareRootText', { - defaultMessage: 'Square root', - }), - value: 'square root', - }, - ], - orientations: [ - { - text: i18n.translate('visTypeTagCloud.vis.editorConfig.orientations.singleText', { - defaultMessage: 'Single', - }), - value: 'single', - }, - { - text: i18n.translate('visTypeTagCloud.vis.editorConfig.orientations.rightAngledText', { - defaultMessage: 'Right angled', - }), - value: 'right angled', - }, - { - text: i18n.translate('visTypeTagCloud.vis.editorConfig.orientations.multipleText', { - defaultMessage: 'Multiple', - }), - value: 'multiple', - }, - ], - }, - optionsTemplate: TagCloudOptions, - schemas: new Schemas([ + }, + requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.DATA], + visualization: TagCloudVisualization, + editorConfig: { + collections: { + scales: [ + { + text: i18n.translate('visTypeTagCloud.vis.editorConfig.scales.linearText', { + defaultMessage: 'Linear', + }), + value: 'linear', + }, + { + text: i18n.translate('visTypeTagCloud.vis.editorConfig.scales.logText', { + defaultMessage: 'Log', + }), + value: 'log', + }, { - group: 'metrics', - name: 'metric', - title: i18n.translate('visTypeTagCloud.vis.schemas.metricTitle', { - defaultMessage: 'Tag size', + text: i18n.translate('visTypeTagCloud.vis.editorConfig.scales.squareRootText', { + defaultMessage: 'Square root', }), - min: 1, - max: 1, - aggFilter: [ - '!std_dev', - '!percentiles', - '!percentile_ranks', - '!derivative', - '!geo_bounds', - '!geo_centroid', - ], - defaults: [{ schema: 'metric', type: 'count' }], + value: 'square root', }, + ], + orientations: [ { - group: 'buckets', - name: 'segment', - title: i18n.translate('visTypeTagCloud.vis.schemas.segmentTitle', { - defaultMessage: 'Tags', + text: i18n.translate('visTypeTagCloud.vis.editorConfig.orientations.singleText', { + defaultMessage: 'Single', }), - min: 1, - max: 1, - aggFilter: ['terms', 'significant_terms'], + value: 'single', }, - ]), + { + text: i18n.translate('visTypeTagCloud.vis.editorConfig.orientations.rightAngledText', { + defaultMessage: 'Right angled', + }), + value: 'right angled', + }, + { + text: i18n.translate('visTypeTagCloud.vis.editorConfig.orientations.multipleText', { + defaultMessage: 'Multiple', + }), + value: 'multiple', + }, + ], }, - useCustomNoDataScreen: true, - }); + optionsTemplate: TagCloudOptions, + schemas: new Schemas([ + { + group: 'metrics', + name: 'metric', + title: i18n.translate('visTypeTagCloud.vis.schemas.metricTitle', { + defaultMessage: 'Tag size', + }), + min: 1, + max: 1, + aggFilter: [ + '!std_dev', + '!percentiles', + '!percentile_ranks', + '!derivative', + '!geo_bounds', + '!geo_centroid', + ], + defaults: [{ schema: 'metric', type: 'count' }], + }, + { + group: 'buckets', + name: 'segment', + title: i18n.translate('visTypeTagCloud.vis.schemas.segmentTitle', { + defaultMessage: 'Tags', + }), + min: 1, + max: 1, + aggFilter: ['terms', 'significant_terms'], + }, + ]), + }, + useCustomNoDataScreen: true, }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js b/src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js index 464cec744eed7..4d029553145da 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/editor_controller.js @@ -19,69 +19,68 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; import { fetchIndexPatternFields } from './lib/fetch_fields'; +import { getSavedObjectsClient, getUISettings, getI18n } from './services'; -export function createEditorController(config, savedObjectsClient) { - return class { - constructor(el, savedObj) { - this.el = el; +export class EditorController { + constructor(el, savedObj) { + this.el = el; - this.state = { - savedObj: savedObj, - vis: savedObj.vis, - isLoaded: false, - }; - } + this.state = { + savedObj: savedObj, + vis: savedObj.vis, + isLoaded: false, + }; + } - fetchDefaultIndexPattern = async () => { - const indexPattern = await savedObjectsClient.get( - 'index-pattern', - config.get('defaultIndex') - ); + fetchDefaultIndexPattern = async () => { + const indexPattern = await getSavedObjectsClient().client.get( + 'index-pattern', + getUISettings().get('defaultIndex') + ); - return indexPattern.attributes; - }; + return indexPattern.attributes; + }; - fetchDefaultParams = async () => { - const { title, timeFieldName } = await this.fetchDefaultIndexPattern(); + fetchDefaultParams = async () => { + const { title, timeFieldName } = await this.fetchDefaultIndexPattern(); - this.state.vis.params.default_index_pattern = title; - this.state.vis.params.default_timefield = timeFieldName; - this.state.vis.fields = await fetchIndexPatternFields(this.state.vis); + this.state.vis.params.default_index_pattern = title; + this.state.vis.params.default_timefield = timeFieldName; + this.state.vis.fields = await fetchIndexPatternFields(this.state.vis); - this.state.isLoaded = true; - }; + this.state.isLoaded = true; + }; - getComponent = () => { - return this.state.vis.type.editorConfig.component; - }; + getComponent = () => { + return this.state.vis.type.editorConfig.component; + }; - async render(params) { - const Component = this.getComponent(); + async render(params) { + const Component = this.getComponent(); + const I18nContext = getI18n().Context; - !this.state.isLoaded && (await this.fetchDefaultParams()); + !this.state.isLoaded && (await this.fetchDefaultParams()); - render( - - {}} - isEditorMode={true} - appState={params.appState} - /> - , - this.el - ); - } + render( + + {}} + isEditorMode={true} + appState={params.appState} + /> + , + this.el + ); + } - destroy() { - unmountComponentAtNode(this.el); - } - }; + destroy() { + unmountComponentAtNode(this.el); + } } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts index 12f14ea3cb816..8740f84dab3b9 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts @@ -20,12 +20,10 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { PersistedState } from 'ui/persisted_state'; -import chrome from 'ui/chrome'; - import { ExpressionFunction, KibanaContext, Render } from '../../../../plugins/expressions/public'; // @ts-ignore -import { createMetricsRequestHandler } from './request_handler'; +import { metricsRequestHandler } from './request_handler'; const name = 'tsvb'; type Context = KibanaContext | null; @@ -68,8 +66,6 @@ export const createMetricsFn = (): ExpressionFunction { - const uiSettings = chrome.getUiSettingsClient(); - const savedObjectsClient = chrome.getSavedObjectsClient(); - const EditorController = createEditorController(uiSettings, savedObjectsClient); - const metricsRequestHandler = createMetricsRequestHandler(uiSettings); - - return visFactory.createReactVisualization({ - name: 'metrics', - title: i18n.translate('visTypeTimeseries.kbnVisTypes.metricsTitle', { defaultMessage: 'TSVB' }), - description: i18n.translate('visTypeTimeseries.kbnVisTypes.metricsDescription', { - defaultMessage: 'Build time-series using a visual pipeline interface', - }), - icon: 'visVisualBuilder', - feedbackMessage: defaultFeedbackMessage, - visConfig: { - defaults: { - id: '61ca57f0-469d-11e7-af02-69e470af7417', - type: PANEL_TYPES.TIMESERIES, - series: [ - { - id: '61ca57f1-469d-11e7-af02-69e470af7417', - color: '#68BC00', - split_mode: 'everything', - metrics: [ - { - id: '61ca57f2-469d-11e7-af02-69e470af7417', - type: 'count', - }, - ], - separate_axis: 0, - axis_position: 'right', - formatter: 'number', - chart_type: 'line', - line_width: 1, - point_size: 1, - fill: 0.5, - stacked: 'none', - }, - ], - time_field: '', - index_pattern: '', - interval: '', - axis_position: 'left', - axis_formatter: 'number', - axis_scale: 'normal', - show_legend: 1, - show_grid: 1, - }, - component: require('./components/vis_editor').VisEditor, - }, - editor: EditorController, - editorConfig: { - component: require('./components/vis_editor').VisEditor, - }, - options: { - showQueryBar: false, - showFilterBar: false, - showIndexSelection: false, +export const metricsVisDefinition = { + name: 'metrics', + title: i18n.translate('visTypeTimeseries.kbnVisTypes.metricsTitle', { defaultMessage: 'TSVB' }), + description: i18n.translate('visTypeTimeseries.kbnVisTypes.metricsDescription', { + defaultMessage: 'Build time-series using a visual pipeline interface', + }), + icon: 'visVisualBuilder', + feedbackMessage: defaultFeedbackMessage, + visConfig: { + defaults: { + id: '61ca57f0-469d-11e7-af02-69e470af7417', + type: PANEL_TYPES.TIMESERIES, + series: [ + { + id: '61ca57f1-469d-11e7-af02-69e470af7417', + color: '#68BC00', + split_mode: 'everything', + metrics: [ + { + id: '61ca57f2-469d-11e7-af02-69e470af7417', + type: 'count', + }, + ], + separate_axis: 0, + axis_position: 'right', + formatter: 'number', + chart_type: 'line', + line_width: 1, + point_size: 1, + fill: 0.5, + stacked: 'none', + }, + ], + time_field: '', + index_pattern: '', + interval: '', + axis_position: 'left', + axis_formatter: 'number', + axis_scale: 'normal', + show_legend: 1, + show_grid: 1, }, - requestHandler: metricsRequestHandler, - responseHandler: 'none', - }); + component: require('./components/vis_editor').VisEditor, + }, + editor: EditorController, + editorConfig: { + component: require('./components/vis_editor').VisEditor, + }, + options: { + showQueryBar: false, + showFilterBar: false, + showIndexSelection: false, + }, + requestHandler: metricsRequestHandler, + responseHandler: 'none', }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts index 42ff653e9bfe9..75a65e131797d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts @@ -16,18 +16,30 @@ * specific language governing permissions and limitations * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + SavedObjectsClientContract, + UiSettingsClientContract, +} from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { createMetricsFn } from './metrics_fn'; -import { createMetricsTypeDefinition } from './metrics_type'; +import { metricsVisDefinition } from './metrics_type'; +import { setSavedObjectsClient, setUISettings, setI18n } from './services'; /** @internal */ export interface MetricsPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; } +export interface MetricsVisualizationDependencies { + uiSettings: UiSettingsClientContract; + savedObjectsClient: SavedObjectsClientContract; +} /** @internal */ export class MetricsPlugin implements Plugin, void> { @@ -42,10 +54,13 @@ export class MetricsPlugin implements Plugin, void> { { expressions, visualizations }: MetricsPluginSetupDependencies ) { expressions.registerFunction(createMetricsFn); - visualizations.types.registerVisualization(createMetricsTypeDefinition); + setUISettings(core.uiSettings); + visualizations.types.createReactVisualization(metricsVisDefinition); } public start(core: CoreStart) { // nothing to do here yet + setSavedObjectsClient(core.savedObjects); + setI18n(core.i18n); } } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js b/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js index 7bd400e8bed15..f4032af1838c1 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/request_handler.js @@ -21,48 +21,47 @@ import { validateInterval } from './lib/validate_interval'; import { timezoneProvider } from 'ui/vis/lib/timezone'; import { timefilter } from 'ui/timefilter'; import { kfetch } from 'ui/kfetch'; +import { getUISettings } from './services'; -export const createMetricsRequestHandler = function (config) { +export const metricsRequestHandler = async ({ uiState, timeRange, filters, query, visParams }) => { + const config = getUISettings(); const timezone = timezoneProvider(config)(); + const uiStateObj = uiState.get(visParams.type, {}); + const parsedTimeRange = timefilter.calculateBounds(timeRange); + const scaledDataFormat = config.get('dateFormat:scaled'); + const dateFormat = config.get('dateFormat'); - return async ({ uiState, timeRange, filters, query, visParams }) => { - const uiStateObj = uiState.get(visParams.type, {}); - const parsedTimeRange = timefilter.calculateBounds(timeRange); - const scaledDataFormat = config.get('dateFormat:scaled'); - const dateFormat = config.get('dateFormat'); + if (visParams && visParams.id && !visParams.isModelInvalid) { + try { + const maxBuckets = config.get('metrics:max_buckets'); - if (visParams && visParams.id && !visParams.isModelInvalid) { - try { - const maxBuckets = config.get('metrics:max_buckets'); + validateInterval(parsedTimeRange, visParams, maxBuckets); - validateInterval(parsedTimeRange, visParams, maxBuckets); + const resp = await kfetch({ + pathname: '/api/metrics/vis/data', + method: 'POST', + body: JSON.stringify({ + timerange: { + timezone, + ...parsedTimeRange, + }, + query, + filters, + panels: [visParams], + state: uiStateObj, + }), + }); - const resp = await kfetch({ - pathname: '/api/metrics/vis/data', - method: 'POST', - body: JSON.stringify({ - timerange: { - timezone, - ...parsedTimeRange, - }, - query, - filters, - panels: [visParams], - state: uiStateObj, - }), - }); - - return { - dateFormat, - scaledDataFormat, - timezone, - ...resp, - }; - } catch (error) { - return Promise.reject(error); - } + return { + dateFormat, + scaledDataFormat, + timezone, + ...resp, + }; + } catch (error) { + return Promise.reject(error); } + } - return Promise.resolve({}); - }; + return Promise.resolve({}); }; diff --git a/src/legacy/ui/public/vis/vis_factory.js b/src/legacy/core_plugins/vis_type_timeseries/public/services.ts similarity index 62% rename from src/legacy/ui/public/vis/vis_factory.js rename to src/legacy/core_plugins/vis_type_timeseries/public/services.ts index 136122f097f38..dcc7de4098bdd 100644 --- a/src/legacy/ui/public/vis/vis_factory.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/services.ts @@ -17,19 +17,15 @@ * under the License. */ -import { BaseVisType, ReactVisType } from './vis_types'; +import { I18nStart, SavedObjectsStart, UiSettingsClientContract } from 'src/core/public'; +import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -export const visFactory = { - createBaseVisualization: (config) => { - return new BaseVisType(config); - }, - createReactVisualization: (config) => { - return new ReactVisType(config); - }, -}; +export const [getUISettings, setUISettings] = createGetterSetter( + 'UISettings' +); -export const VisFactoryProvider = () => { - return { - ...visFactory, - }; -}; +export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter( + 'SavedObjectsClient' +); + +export const [getI18n, setI18n] = createGetterSetter('I18n'); diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js index 191f35d2e03ea..6a1a5431f2e51 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js @@ -67,9 +67,7 @@ describe('VegaVisualizations', () => { if(!visRegComplete) { visRegComplete = true; - visualizationsSetup.types.registerVisualization(() => - createVegaTypeDefinition(vegaVisualizationDependencies) - ); + visualizationsSetup.types.createBaseVisualization(createVegaTypeDefinition(vegaVisualizationDependencies)); } diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts index 67fbba7f161d3..9001164afe820 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts @@ -61,7 +61,7 @@ export class VegaPlugin implements Plugin, void> { expressions.registerFunction(() => createVegaFn(visualizationDependencies)); - visualizations.types.registerVisualization(() => + visualizations.types.createBaseVisualization( createVegaTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts index 0d5290ddbefc7..9ab5f820cec31 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts @@ -18,13 +18,12 @@ */ import { i18n } from '@kbn/i18n'; -import { Status } from 'ui/vis/update_status'; // @ts-ignore import { DefaultEditorSize } from 'ui/vis/editor_size'; // @ts-ignore import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; +import { Status } from '../../visualizations/public'; -import { visFactory } from '../../visualizations/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; @@ -38,7 +37,7 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen const requestHandler = createVegaRequestHandler(dependencies); const visualization = createVegaVisualization(dependencies); - return visFactory.createBaseVisualization({ + return { name: 'vega', title: 'Vega', description: i18n.translate('visTypeVega.type.vegaDescription', { @@ -63,5 +62,5 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen }, stage: 'experimental', feedbackMessage: defaultFeedbackMessage, - }); + }; }; diff --git a/src/legacy/core_plugins/visualizations/public/expressions/visualization_renderer.tsx b/src/legacy/core_plugins/visualizations/public/expressions/visualization_renderer.tsx index f15cdf23fe15b..40648a137c141 100644 --- a/src/legacy/core_plugins/visualizations/public/expressions/visualization_renderer.tsx +++ b/src/legacy/core_plugins/visualizations/public/expressions/visualization_renderer.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; // @ts-ignore import { Vis } from '../../../../ui/public/visualize/loader/vis'; -import { Visualization } from '../../../../ui/public/visualize/components'; +import { Visualization } from '../../../visualizations/public/np_ready/public/components'; export const visualization = () => ({ name: 'visualization', diff --git a/src/legacy/core_plugins/visualizations/public/index.ts b/src/legacy/core_plugins/visualizations/public/index.ts index ca79f547890f9..f38c03c50c307 100644 --- a/src/legacy/core_plugins/visualizations/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/index.ts @@ -26,23 +26,8 @@ // @ts-ignore Used only by tsvb, vega, input control vis export { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; // @ts-ignore -export { visFactory } from 'ui/vis/vis_factory'; -// @ts-ignore export { DefaultEditorSize } from 'ui/vis/editor_size'; -/** - * Legacy types which haven't been moved to this plugin yet, but - * should be eventually. - * - * @public - */ -import * as types from 'ui/vis/vis'; -export type Vis = types.Vis; -export type VisParams = types.VisParams; -export type VisState = types.VisState; -export { VisualizationController } from 'ui/vis/vis_types/vis_type'; -export { Status } from 'ui/vis/update_status'; - /** * Static np-ready code, re-exported here so consumers can import from * `src/legacy/core_plugins/visualizations/public` @@ -50,6 +35,3 @@ export { Status } from 'ui/vis/update_status'; * @public */ export * from './np_ready/public'; - -// for backwards compatibility with 7.3 -export { setup as visualizations } from './np_ready/public/legacy'; diff --git a/src/legacy/ui/public/vis/push_filters.js b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts similarity index 56% rename from src/legacy/ui/public/vis/push_filters.js rename to src/legacy/core_plugins/visualizations/public/legacy_imports.ts index 771de14f9446d..92d8ac2c7db3a 100644 --- a/src/legacy/ui/public/vis/push_filters.js +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -17,11 +17,14 @@ * under the License. */ -import _ from 'lodash'; - -// TODO: should it be here or in vis filters (only place where it's used). -// $newFilters is not defined by filter_bar as well. -export function pushFilterBarFilters($state, filters) { - if (!_.isObject($state)) throw new Error('pushFilters requires a state object'); - $state.$newFilters = filters; -} +export { PersistedState } from '../../../ui/public/persisted_state'; +export { SearchError } from '../../../ui/public/courier/search_strategy/search_error'; +export { AggConfig } from '../../../ui/public/agg_types/agg_config'; +export { AggConfigs } from '../../../ui/public/agg_types/agg_configs'; +export { + isDateHistogramBucketAggConfig, + setBounds, +} from '../../../ui/public/agg_types/buckets/date_histogram'; +export { createFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; +export { I18nContext } from '../../../ui/public/i18n'; +import '../../../ui/public/directives/bind'; diff --git a/src/legacy/ui/public/vis/vis_filters/index.js b/src/legacy/core_plugins/visualizations/public/legacy_mocks.ts similarity index 88% rename from src/legacy/ui/public/vis/vis_filters/index.js rename to src/legacy/core_plugins/visualizations/public/legacy_mocks.ts index 1236e88a52803..e6ca678db563d 100644 --- a/src/legacy/ui/public/vis/vis_filters/index.js +++ b/src/legacy/core_plugins/visualizations/public/legacy_mocks.ts @@ -17,4 +17,4 @@ * under the License. */ -export { VisFiltersProvider, createFilter, createFiltersFromEvent, onBrushEvent } from './vis_filters'; +export { searchSourceMock } from '../../../ui/public/courier/search_source/mocks'; diff --git a/src/legacy/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/__snapshots__/visualization_noresults.test.js.snap similarity index 100% rename from src/legacy/ui/public/visualize/components/__snapshots__/visualization_noresults.test.js.snap rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/__snapshots__/visualization_noresults.test.js.snap diff --git a/src/legacy/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/__snapshots__/visualization_requesterror.test.js.snap similarity index 100% rename from src/legacy/ui/public/visualize/components/__snapshots__/visualization_requesterror.test.js.snap rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/__snapshots__/visualization_requesterror.test.js.snap diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/components/_index.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/_index.scss new file mode 100644 index 0000000000000..532e8106b023f --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/_index.scss @@ -0,0 +1 @@ +@import 'visualization'; diff --git a/src/legacy/ui/public/visualize/components/_visualization.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/_visualization.scss similarity index 100% rename from src/legacy/ui/public/visualize/components/_visualization.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/_visualization.scss diff --git a/src/legacy/ui/public/visualize/components/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/index.ts similarity index 100% rename from src/legacy/ui/public/visualize/components/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/index.ts diff --git a/src/legacy/ui/public/visualize/components/visualization.test.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.test.js similarity index 100% rename from src/legacy/ui/public/visualize/components/visualization.test.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.test.js diff --git a/src/legacy/ui/public/visualize/components/visualization.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.tsx similarity index 95% rename from src/legacy/ui/public/visualize/components/visualization.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.tsx index 26c894f914910..5a9a1830ebdf3 100644 --- a/src/legacy/ui/public/visualize/components/visualization.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.tsx @@ -20,12 +20,12 @@ import { get } from 'lodash'; import React from 'react'; -import { PersistedState } from '../../persisted_state'; -import { memoizeLast } from '../../utils/memoize'; -import { Vis } from '../../vis'; +import { PersistedState } from '../../../legacy_imports'; +import { memoizeLast } from '../legacy/memoize'; import { VisualizationChart } from './visualization_chart'; import { VisualizationNoResults } from './visualization_noresults'; import { VisualizationRequestError } from './visualization_requesterror'; +import { Vis } from '..'; function shouldShowNoResultsMessage(vis: Vis, visData: any): boolean { const requiresSearch = get(vis, 'type.requiresSearch'); diff --git a/src/legacy/ui/public/visualize/components/visualization_chart.test.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.test.js similarity index 100% rename from src/legacy/ui/public/visualize/components/visualization_chart.test.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.test.js diff --git a/src/legacy/ui/public/visualize/components/visualization_chart.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.tsx similarity index 95% rename from src/legacy/ui/public/visualize/components/visualization_chart.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.tsx index 8aec7adeaec9a..95fd31049d233 100644 --- a/src/legacy/ui/public/visualize/components/visualization_chart.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.tsx @@ -21,10 +21,10 @@ import React from 'react'; import * as Rx from 'rxjs'; import { debounceTime, filter, share, switchMap } from 'rxjs/operators'; -import { PersistedState } from '../../persisted_state'; -import { ResizeChecker } from '../../../../../plugins/kibana_utils/public'; -import { Vis, VisualizationController } from '../../vis'; -import { getUpdateStatus } from '../../vis/update_status'; +import { PersistedState } from '../../../legacy_imports'; +import { Vis, VisualizationController } from '../vis'; +import { getUpdateStatus } from '../legacy/update_status'; +import { ResizeChecker } from '../../../../../../../plugins/kibana_utils/public'; interface VisualizationChartProps { onInit?: () => void; diff --git a/src/legacy/ui/public/visualize/components/visualization_noresults.test.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_noresults.test.js similarity index 100% rename from src/legacy/ui/public/visualize/components/visualization_noresults.test.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_noresults.test.js diff --git a/src/legacy/ui/public/visualize/components/visualization_noresults.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_noresults.tsx similarity index 90% rename from src/legacy/ui/public/visualize/components/visualization_noresults.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_noresults.tsx index 8ba3f66ec4d86..5a964caa46b4b 100644 --- a/src/legacy/ui/public/visualize/components/visualization_noresults.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_noresults.tsx @@ -19,7 +19,6 @@ import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; -import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; interface VisualizationNoResultsProps { onInit?: () => void; @@ -58,8 +57,5 @@ export class VisualizationNoResults extends React.Component void; @@ -59,8 +58,5 @@ export class VisualizationRequestError extends React.Component ({ npStart: { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts index 480796c377175..4558621dc6615 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export * from './filters_service'; +// @ts-ignore +export * from './vis_filters'; diff --git a/src/legacy/ui/public/vis/vis_filters/vis_filters.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js similarity index 84% rename from src/legacy/ui/public/vis/vis_filters/vis_filters.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js index 18d633e1b5fb2..9e72cb3402a5a 100644 --- a/src/legacy/ui/public/vis/vis_filters/vis_filters.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js @@ -17,10 +17,8 @@ * under the License. */ -import _ from 'lodash'; -import { pushFilterBarFilters } from '../push_filters'; import { onBrushEvent } from './brush_event'; -import { uniqFilters, esFilters } from '../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../../plugins/data/public'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -104,20 +102,4 @@ const createFiltersFromEvent = (event) => { return filters; }; -const VisFiltersProvider = (getAppState, $timeout) => { - - const pushFilters = (filters, simulate) => { - const appState = getAppState(); - if (filters.length && !simulate) { - pushFilterBarFilters(appState, uniqFilters(filters)); - // to trigger angular digest cycle, we can get rid of this once we have either new filterManager or actions API - $timeout(_.noop, 0); - } - }; - - return { - pushFilters, - }; -}; - -export { VisFiltersProvider, createFilter, createFiltersFromEvent, onBrushEvent }; +export { createFilter, createFiltersFromEvent, onBrushEvent }; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index ceb0ca5316354..2e9d055858a48 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -43,4 +43,12 @@ export function plugin(initializerContext: PluginInitializerContext) { } /** @public static code */ -// TODO once items are moved from ui/vis into this service +export { Vis, VisParams, VisState } from './vis'; +export * from './filters'; + +export { Status } from './legacy/update_status'; +export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/build_pipeline'; + +// @ts-ignore +export { updateOldState } from './legacy/vis_update_state'; +export { calculateObjectHash } from './legacy/calculate_object_hash'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts index 16cec5d2d9e91..1e86aa64d1fa8 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts @@ -21,18 +21,11 @@ import { PluginInitializerContext } from 'src/core/public'; /* eslint-disable @kbn/eslint/no-restricted-paths */ import { npSetup, npStart } from 'ui/new_platform'; -// @ts-ignore -import { VisFiltersProvider, createFilter } from 'ui/vis/vis_filters'; /* eslint-enable @kbn/eslint/no-restricted-paths */ import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); -export const setup = pluginInstance.setup(npSetup.core, { - __LEGACY: { - VisFiltersProvider, - createFilter, - }, -}); +export const setup = pluginInstance.setup(npSetup.core); export const start = pluginInstance.start(npStart.core); diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.ts.snap b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__snapshots__/build_pipeline.test.ts.snap similarity index 100% rename from src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.ts.snap rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__snapshots__/build_pipeline.test.ts.snap diff --git a/src/legacy/ui/public/vis/__tests__/_vis.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js similarity index 96% rename from src/legacy/ui/public/vis/__tests__/_vis.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js index 1d5e2de6dafe3..bd8ce8381608c 100644 --- a/src/legacy/ui/public/vis/__tests__/_vis.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js @@ -20,9 +20,9 @@ import _ from 'lodash'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { Vis } from '..'; +import { Vis } from '../..'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { start as visualizations } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy'; +import { start as visualizations } from '../../legacy'; describe('Vis Class', function () { let indexPattern; diff --git a/src/legacy/ui/public/vis/__tests__/vis_types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js similarity index 96% rename from src/legacy/ui/public/vis/__tests__/vis_types/base_vis_type.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js index d2e545bde8241..5e03b205e76e4 100644 --- a/src/legacy/ui/public/vis/__tests__/vis_types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/base_vis_type.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { BaseVisType } from '../../vis_types/base_vis_type'; +import { BaseVisType } from '../../../types/base_vis_type'; describe('Base Vis Type', function () { beforeEach(ngMock.module('kibana')); diff --git a/src/legacy/ui/public/vis/__tests__/vis_types/react_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js similarity index 96% rename from src/legacy/ui/public/vis/__tests__/vis_types/react_vis_type.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js index b2478655d1cfe..bc16c6acbc20c 100644 --- a/src/legacy/ui/public/vis/__tests__/vis_types/react_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_types/react_vis_type.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { ReactVisType } from '../../vis_types/react_vis_type'; +import { ReactVisType } from '../../../types/react_vis_type'; describe('React Vis Type', function () { diff --git a/src/legacy/ui/public/vis/__tests__/vis_update_objs/gauge_objs.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_update_objs/gauge_objs.js similarity index 100% rename from src/legacy/ui/public/vis/__tests__/vis_update_objs/gauge_objs.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/vis_update_objs/gauge_objs.js diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts similarity index 98% rename from src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts index 608a8b9ce8aa7..e733bad2c0127 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts @@ -26,9 +26,9 @@ import { SchemaConfig, Schemas, } from './build_pipeline'; -import { Vis, VisState } from 'ui/vis'; -import { AggConfig } from 'ui/agg_types/agg_config'; -import { searchSourceMock } from '../../../courier/search_source/mocks'; +import { Vis, VisState } from '..'; +import { AggConfig } from '../../../legacy_imports'; +import { searchSourceMock } from '../../../legacy_mocks'; jest.mock('ui/new_platform'); jest.mock('ui/agg_types/buckets/date_histogram', () => ({ diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts similarity index 98% rename from src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts index ca9540b4d3737..0f9e9c11a9dbc 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts @@ -19,13 +19,17 @@ import { cloneDeep, get } from 'lodash'; // @ts-ignore -import { setBounds } from 'ui/agg_types'; -import { AggConfig, Vis, VisParams, VisState } from 'ui/vis'; -import { isDateHistogramBucketAggConfig } from 'ui/agg_types/buckets/date_histogram'; import moment from 'moment'; import { SerializedFieldFormat } from 'src/plugins/expressions/public'; -import { SearchSourceContract } from '../../../courier/types'; -import { createFormat } from './utilities'; +import { + AggConfig, + setBounds, + isDateHistogramBucketAggConfig, + createFormat, +} from '../../../legacy_imports'; +// eslint-disable-next-line +import { SearchSourceContract } from '../../../../../../ui/public/courier/search_source/search_source'; +import { Vis, VisParams, VisState } from '..'; interface SchemaConfigParams { precision?: number; diff --git a/src/legacy/ui/public/vis/lib/calculate_object_hash.d.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.d.ts similarity index 100% rename from src/legacy/ui/public/vis/lib/calculate_object_hash.d.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.d.ts diff --git a/src/legacy/ui/public/vis/lib/calculate_object_hash.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.js similarity index 100% rename from src/legacy/ui/public/vis/lib/calculate_object_hash.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.js diff --git a/src/legacy/ui/public/utils/memoize.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/memoize.test.ts similarity index 100% rename from src/legacy/ui/public/utils/memoize.test.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/memoize.test.ts diff --git a/src/legacy/ui/public/utils/memoize.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/memoize.ts similarity index 100% rename from src/legacy/ui/public/utils/memoize.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/memoize.ts diff --git a/src/legacy/ui/public/vis/update_status.test.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.test.js similarity index 100% rename from src/legacy/ui/public/vis/update_status.test.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.test.js diff --git a/src/legacy/ui/public/vis/update_status.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts similarity index 95% rename from src/legacy/ui/public/vis/update_status.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts index f5e162f50dcf6..6d32a6df5f1ec 100644 --- a/src/legacy/ui/public/vis/update_status.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts @@ -17,9 +17,9 @@ * under the License. */ -import { PersistedState } from '../persisted_state'; -import { calculateObjectHash } from './lib/calculate_object_hash'; -import { Vis } from './vis'; +import { PersistedState } from '../../../legacy_imports'; +import { calculateObjectHash } from './calculate_object_hash'; +import { Vis } from '../vis'; enum Status { AGGS = 'aggs', diff --git a/src/legacy/ui/public/vis/vis_update.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/vis_update.js similarity index 100% rename from src/legacy/ui/public/vis/vis_update.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/vis_update.js diff --git a/src/legacy/ui/public/vis/vis_update_state.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/vis_update_state.js similarity index 100% rename from src/legacy/ui/public/vis/vis_update_state.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/vis_update_state.js diff --git a/src/legacy/ui/public/vis/vis_update_state.test.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/vis_update_state.test.js similarity index 100% rename from src/legacy/ui/public/vis/vis_update_state.test.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/vis_update_state.test.js diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts index 5d7ab12a677cf..88c5768a0b4e4 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -29,18 +29,10 @@ import { VisualizationsSetup, VisualizationsStart } from './'; import { VisualizationsPlugin } from './plugin'; import { coreMock } from '../../../../../../core/public/mocks'; -/* eslint-disable */ -// @ts-ignore -import { VisFiltersProvider, createFilter } from 'ui/vis/vis_filters'; -/* eslint-enable */ - const createSetupContract = (): VisualizationsSetup => ({ - filters: { - VisFiltersProvider: jest.fn(), - createFilter: jest.fn(), - }, types: { - registerVisualization: jest.fn(), + createBaseVisualization: jest.fn(), + createReactVisualization: jest.fn(), registerAlias: jest.fn(), hideTypes: jest.fn(), }, @@ -57,12 +49,7 @@ const createStartContract = (): VisualizationsStart => ({ const createInstance = async () => { const plugin = new VisualizationsPlugin({} as PluginInitializerContext); - const setup = plugin.setup(coreMock.createSetup(), { - __LEGACY: { - VisFiltersProvider, - createFilter, - }, - }); + const setup = plugin.setup(coreMock.createSetup()); const doStart = () => plugin.start(coreMock.createStart()); return { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 0e77c3ce88385..ccf6aaf152ea4 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -17,21 +17,8 @@ * under the License. */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; - -import { FiltersService, FiltersSetup } from './filters'; import { TypesService, TypesSetup, TypesStart } from './types'; - -/** - * Interface for any dependencies on other plugins' contracts. - * - * @internal - */ -interface VisualizationsPluginSetupDependencies { - __LEGACY: { - VisFiltersProvider: any; - createFilter: any; - }; -} +import { setUISettings, setTypes, setI18n } from './services'; /** * Interface for this plugin's returned setup/start contracts. @@ -39,7 +26,6 @@ interface VisualizationsPluginSetupDependencies { * @public */ export interface VisualizationsSetup { - filters: FiltersSetup; types: TypesSetup; } @@ -56,34 +42,28 @@ export interface VisualizationsStart { * * @internal */ -export class VisualizationsPlugin - implements - Plugin { - private readonly filters: FiltersService = new FiltersService(); +export class VisualizationsPlugin implements Plugin { private readonly types: TypesService = new TypesService(); constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { __LEGACY }: VisualizationsPluginSetupDependencies) { - const { VisFiltersProvider, createFilter } = __LEGACY; - + public setup(core: CoreSetup) { + setUISettings(core.uiSettings); return { - filters: this.filters.setup({ - VisFiltersProvider, - createFilter, - }), types: this.types.setup(), }; } public start(core: CoreStart) { + setI18n(core.i18n); + const types = this.types.start(); + setTypes(types); return { - types: this.types.start(), + types, }; } public stop() { - this.filters.stop(); this.types.stop(); } } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/filters_service.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts similarity index 63% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/filters_service.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts index 51709f365dbbd..63afbca71a280 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/filters_service.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/services.ts @@ -17,28 +17,14 @@ * under the License. */ -interface SetupDependecies { - VisFiltersProvider: any; - createFilter: any; -} +import { I18nStart, UiSettingsClientContract } from 'src/core/public'; +import { TypesStart } from './types'; +import { createGetterSetter } from '../../../../../../plugins/kibana_utils/public'; -/** - * Vis Filters Service - * - * @internal - */ -export class FiltersService { - public setup({ VisFiltersProvider, createFilter }: SetupDependecies) { - return { - VisFiltersProvider, - createFilter, - }; - } +export const [getUISettings, setUISettings] = createGetterSetter( + 'UISettings' +); - public stop() { - // nothing to do here yet - } -} +export const [getTypes, setTypes] = createGetterSetter('Types'); -/** @public */ -export type FiltersSetup = ReturnType; +export const [getI18n, setI18n] = createGetterSetter('I18n'); diff --git a/src/legacy/ui/public/vis/vis_types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js similarity index 97% rename from src/legacy/ui/public/vis/vis_types/base_vis_type.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js index 30d806bc305af..2dc657ecde05b 100644 --- a/src/legacy/ui/public/vis/vis_types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { createFiltersFromEvent, onBrushEvent } from '../vis_filters'; +import { createFiltersFromEvent, onBrushEvent } from '../filters'; export class BaseVisType { constructor(opts = {}) { diff --git a/src/legacy/ui/public/vis/vis_types/react_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/react_vis_type.js similarity index 93% rename from src/legacy/ui/public/vis/vis_types/react_vis_type.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/types/react_vis_type.js index 29f809a65edda..2566e25c17343 100644 --- a/src/legacy/ui/public/vis/vis_types/react_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/react_vis_type.js @@ -19,11 +19,9 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import chrome from '../../chrome'; -import { I18nContext } from '../../i18n'; +import { getUISettings, getI18n } from '../services'; import { BaseVisType } from './base_vis_type'; - class ReactVisController { constructor(element, vis) { this.el = element; @@ -33,9 +31,11 @@ class ReactVisController { render(visData, visParams, updateStatus) { this.visData = visData; + const I18nContext = getI18n().Context; + return new Promise((resolve) => { const Component = this.vis.type.visConfig.component; - const config = chrome.getUiSettingsClient(); + const config = getUISettings(); render( = {}; private unregisteredHiddenTypes: string[] = []; + public setup() { - return { - registerVisualization: (registerFn: () => VisType) => { - const visDefinition = registerFn(); - if (this.unregisteredHiddenTypes.includes(visDefinition.name)) { - visDefinition.hidden = true; - } + const registerVisualization = (registerFn: () => VisType) => { + const visDefinition = registerFn(); + if (this.unregisteredHiddenTypes.includes(visDefinition.name)) { + visDefinition.hidden = true; + } - if (this.types[visDefinition.name]) { - throw new Error('type already exists!'); - } - this.types[visDefinition.name] = visDefinition; + if (this.types[visDefinition.name]) { + throw new Error('type already exists!'); + } + this.types[visDefinition.name] = visDefinition; + }; + return { + createBaseVisualization: (config: any) => { + const vis = new BaseVisType(config); + registerVisualization(() => vis); + }, + createReactVisualization: (config: any) => { + const vis = new ReactVisType(config); + registerVisualization(() => vis); }, registerAlias: visTypeAliasRegistry.add, hideTypes: (typeNames: string[]) => { diff --git a/src/legacy/ui/public/vis/vis.d.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.d.ts similarity index 76% rename from src/legacy/ui/public/vis/vis.d.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis.d.ts index e16562641801e..6e6a2174d6ad1 100644 --- a/src/legacy/ui/public/vis/vis.d.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.d.ts @@ -17,8 +17,9 @@ * under the License. */ -import { VisType } from './vis_types/vis_type'; -import { AggConfigs } from '../agg_types/agg_configs'; +import { VisType } from './types'; +import { AggConfigs } from '../../legacy_imports'; +import { Status } from './legacy/update_status'; export interface Vis { type: VisType; @@ -40,3 +41,10 @@ export interface VisState { params: VisParams; aggs: AggConfigs; } + +export declare class VisualizationController { + constructor(element: HTMLElement, vis: Vis); + public render(visData: any, visParams: any, update: { [key in Status]: boolean }): Promise; + public destroy(): void; + public isLoaded?(): Promise | void; +} diff --git a/src/legacy/ui/public/vis/vis.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.js similarity index 91% rename from src/legacy/ui/public/vis/vis.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/vis.js index 304289a5cfa07..558fff7d0076e 100644 --- a/src/legacy/ui/public/vis/vis.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.js @@ -29,16 +29,9 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; -import '../render_complete/directive'; -import { AggConfigs } from '../agg_types/agg_configs'; -import { PersistedState } from '../persisted_state'; -import { updateVisualizationConfig } from './vis_update'; -import { SearchSource } from '../courier'; -import { start as visualizations } from '../../../core_plugins/visualizations/public/np_ready/public/legacy'; - -import '../directives/bind'; - -const visTypes = visualizations.types; +import { AggConfigs, PersistedState } from '../../legacy_imports'; +import { updateVisualizationConfig } from './legacy/vis_update'; +import { getTypes } from './services'; class Vis extends EventEmitter { constructor(indexPattern, visState) { @@ -50,6 +43,7 @@ class Vis extends EventEmitter { type: visState }; } + this.indexPattern = indexPattern; this._setUiState(new PersistedState()); this.setCurrentState(visState); @@ -60,7 +54,6 @@ class Vis extends EventEmitter { this.sessionState = {}; this.API = { - SearchSource: SearchSource, events: { filter: data => this.eventsSubject.next({ name: 'filterBucket', data }), brush: data => this.eventsSubject.next({ name: 'brush', data }), @@ -72,7 +65,7 @@ class Vis extends EventEmitter { this.title = state.title || ''; const type = state.type || this.type; if (_.isString(type)) { - this.type = visTypes.get(type); + this.type = getTypes().get(type); if (!this.type) { throw new Error(`Invalid type "${type}"`); } diff --git a/src/legacy/ui/public/agg_types/agg_configs.ts b/src/legacy/ui/public/agg_types/agg_configs.ts index 2f6951891f84d..b4ea0ec8bc465 100644 --- a/src/legacy/ui/public/agg_types/agg_configs.ts +++ b/src/legacy/ui/public/agg_types/agg_configs.ts @@ -28,13 +28,14 @@ import _ from 'lodash'; import { TimeRange } from 'src/plugins/data/public'; -import { Schemas } from '../visualize/loader/pipeline_helpers/build_pipeline'; import { Schema } from '../vis/editors/default/schemas'; import { AggConfig, AggConfigOptions } from './agg_config'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; import { IndexPattern } from '../../../core_plugins/data/public'; import { SearchSourceContract, FetchOptions } from '../courier/types'; +type Schemas = Record; + function removeParentAggs(obj: any) { for (const prop in obj) { if (prop === 'parentAggs') delete obj[prop]; diff --git a/src/legacy/ui/public/agg_types/buckets/date_histogram.ts b/src/legacy/ui/public/agg_types/buckets/date_histogram.ts index 03e358af5f1f0..6a87b2e88ac4c 100644 --- a/src/legacy/ui/public/agg_types/buckets/date_histogram.ts +++ b/src/legacy/ui/public/agg_types/buckets/date_histogram.ts @@ -21,7 +21,7 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; import { BUCKET_TYPES } from 'ui/agg_types/buckets/bucket_agg_types'; -import chrome from '../../chrome'; +import { npStart } from 'ui/new_platform'; import { BucketAggParam, BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { createFilterDateHistogram } from './create_filter/date_histogram'; import { intervalOptions } from './_interval_options'; @@ -39,7 +39,6 @@ import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; // @ts-ignore import { TimeBuckets } from '../../time_buckets'; -const config = chrome.getUiSettingsClient(); const detectedTimezone = moment.tz.guess(); const tzOffset = moment().format('Z'); @@ -224,6 +223,7 @@ export const dateHistogramBucketAgg = new BucketAggType config.get(...args); +const getConfig = (...args) => npStart.core.uiSettings.get(...args); function isValidMoment(m) { return m && ('isValid' in m) && m.isValid(); @@ -238,14 +235,14 @@ TimeBuckets.prototype.getInterval = function (useNormalizedEsInterval = true) { function readInterval() { const interval = self._i; if (moment.isDuration(interval)) return interval; - return calcAutoIntervalNear(config.get('histogram:barTarget'), Number(duration)); + return calcAutoIntervalNear(getConfig('histogram:barTarget'), Number(duration)); } // check to see if the interval should be scaled, and scale it if so function maybeScaleInterval(interval) { if (!self.hasBounds()) return interval; - const maxLength = config.get('histogram:maxBars'); + const maxLength = getConfig('histogram:maxBars'); const approxLen = duration / interval; let scaled; @@ -299,7 +296,7 @@ TimeBuckets.prototype.getInterval = function (useNormalizedEsInterval = true) { */ TimeBuckets.prototype.getScaledDateFormat = function () { const interval = this.getInterval(); - const rules = config.get('dateFormat:scaled'); + const rules = getConfig('dateFormat:scaled'); for (let i = rules.length - 1; i >= 0; i--) { const rule = rules[i]; @@ -308,7 +305,7 @@ TimeBuckets.prototype.getScaledDateFormat = function () { } } - return config.get('dateFormat'); + return getConfig('dateFormat'); }; TimeBuckets.prototype.getScaledDateFormatter = function () { diff --git a/src/legacy/ui/public/vis/__tests__/index.js b/src/legacy/ui/public/vis/__tests__/index.js index 93a0bf026ae5d..46074f2c5197b 100644 --- a/src/legacy/ui/public/vis/__tests__/index.js +++ b/src/legacy/ui/public/vis/__tests__/index.js @@ -19,6 +19,3 @@ import './_agg_config'; import './_agg_configs'; -import './_vis'; -describe('Vis Component', function () { -}); diff --git a/src/legacy/ui/public/vis/index.d.ts b/src/legacy/ui/public/vis/index.d.ts index 791ce2563e0f1..85798549691a5 100644 --- a/src/legacy/ui/public/vis/index.d.ts +++ b/src/legacy/ui/public/vis/index.d.ts @@ -18,5 +18,4 @@ */ export { AggConfig } from '../agg_types/agg_config'; -export { Vis, VisParams, VisState } from './vis'; -export { VisualizationController, VisType } from './vis_types/vis_type'; +export { Vis, VisParams, VisState, VisType } from '../../../core_plugins/visualizations/public'; diff --git a/src/legacy/ui/public/vis/index.js b/src/legacy/ui/public/vis/index.js index 05cd030f7d100..aaee86c378984 100644 --- a/src/legacy/ui/public/vis/index.js +++ b/src/legacy/ui/public/vis/index.js @@ -17,4 +17,4 @@ * under the License. */ -export { Vis } from './vis'; +export { Vis } from '../../../core_plugins/visualizations/public/np_ready/public/vis'; diff --git a/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js b/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js index afb3fea15a430..4ad579e1e45f9 100644 --- a/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js +++ b/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js @@ -21,7 +21,7 @@ import $ from 'jquery'; import _ from 'lodash'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { Vis } from '../../vis'; +import { Vis } from '../../../../../core_plugins/visualizations/public/np_ready/public/vis'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('visualize_legend directive', function () { diff --git a/src/legacy/ui/public/vis/vis_types/index.js b/src/legacy/ui/public/vis/vis_types/index.js index 9c4ae82d58e6f..113aa903df52f 100644 --- a/src/legacy/ui/public/vis/vis_types/index.js +++ b/src/legacy/ui/public/vis/vis_types/index.js @@ -17,7 +17,7 @@ * under the License. */ -import { BaseVisType } from './base_vis_type'; -import { ReactVisType } from './react_vis_type'; +import { BaseVisType } from '../../../../core_plugins/visualizations/public/np_ready/public/types/base_vis_type'; +import { ReactVisType } from '../../../../core_plugins/visualizations/public/np_ready/public/types/react_vis_type'; export { BaseVisType, ReactVisType }; diff --git a/src/legacy/ui/public/vis/vis_types/vis_type.ts b/src/legacy/ui/public/vis/vis_types/vis_type.ts deleted file mode 100644 index 9d06409fda622..0000000000000 --- a/src/legacy/ui/public/vis/vis_types/vis_type.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Status } from '../update_status'; -import { Vis } from '..'; -export { VisType } from '../../../../core_plugins/visualizations/public'; - -export declare class VisualizationController { - constructor(element: HTMLElement, vis: Vis); - public render(visData: any, visParams: any, update: { [key in Status]: boolean }): Promise; - public destroy(): void; - public isLoaded?(): Promise | void; -} diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js index ce94c3a5f68ab..3d054b8f8a2fb 100644 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import html from './vislib_vis_legend.html'; import { Data } from '../../vislib/lib/data'; import { uiModules } from '../../modules'; -import { createFiltersFromEvent } from '../vis_filters'; +import { createFiltersFromEvent } from '../../../../core_plugins/visualizations/public'; import { htmlIdGenerator, keyCodes } from '@elastic/eui'; import { getTableAggs } from '../../visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/ui/public/visualize/_index.scss b/src/legacy/ui/public/visualize/_index.scss index 192091fb04e3c..c528c1e37b412 100644 --- a/src/legacy/ui/public/visualize/_index.scss +++ b/src/legacy/ui/public/visualize/_index.scss @@ -1 +1 @@ -@import './components/index'; +@import '../../../core_plugins/visualizations/public/np_ready/public/components/index'; diff --git a/src/legacy/ui/public/visualize/components/_index.scss b/src/legacy/ui/public/visualize/components/_index.scss deleted file mode 100644 index 99c357b53952f..0000000000000 --- a/src/legacy/ui/public/visualize/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './visualization'; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts index a1292c59ac61d..f19940726ef2d 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { buildPipeline } from './build_pipeline'; +export { buildPipeline } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline'; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts index f49e0f08e8732..377e2cd97b72e 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts @@ -26,7 +26,6 @@ import { SerializedFieldFormat } from 'src/plugins/expressions/public'; import { IFieldFormatId, FieldFormat } from '../../../../../../plugins/data/public'; import { tabifyGetColumns } from '../../../agg_response/tabify/_get_columns'; -import chrome from '../../../chrome'; import { dateRange } from '../../../utils/date_range'; import { ipRange } from '../../../utils/ip_range'; import { DateRangeKey } from '../../../agg_types/buckets/date_range'; @@ -146,7 +145,7 @@ export const getFormat: FormatFactory = mapping => { const parsedUrl = { origin: window.location.origin, pathname: window.location.pathname, - basePath: chrome.getBasePath(), + basePath: npStart.core.http.basePath, }; // @ts-ignore return format.convert(val, undefined, undefined, parsedUrl); @@ -163,7 +162,7 @@ export const getFormat: FormatFactory = mapping => { const parsedUrl = { origin: window.location.origin, pathname: window.location.pathname, - basePath: chrome.getBasePath(), + basePath: npStart.core.http.basePath, }; // @ts-ignore return format.convert(val, type, undefined, parsedUrl); diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js index c2d8ed7f5f9c1..c24dd077b447e 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js @@ -17,37 +17,31 @@ * under the License. */ -import { visFactory } from 'ui/vis/vis_factory'; - import { SelfChangingEditor } from './self_changing_editor'; import { SelfChangingComponent } from './self_changing_components'; import { setup as visualizations } from '../../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/legacy'; -function SelfChangingVisType() { - return visFactory.createReactVisualization({ - name: 'self_changing_vis', - title: 'Self Changing Vis', - icon: 'visControls', - description: 'This visualization is able to change its own settings, that you could also set in the editor.', - visConfig: { - component: SelfChangingComponent, - defaults: { - counter: 0, - }, - }, - editorConfig: { - optionTabs: [ - { - name: 'options', - title: 'Options', - editor: SelfChangingEditor, - }, - ], +visualizations.types.createReactVisualization({ + name: 'self_changing_vis', + title: 'Self Changing Vis', + icon: 'visControls', + description: 'This visualization is able to change its own settings, that you could also set in the editor.', + visConfig: { + component: SelfChangingComponent, + defaults: { + counter: 0, }, - requestHandler: 'none', - }); -} - -visualizations.types.registerVisualization(SelfChangingVisType); + }, + editorConfig: { + optionTabs: [ + { + name: 'options', + title: 'Options', + editor: SelfChangingEditor, + }, + ], + }, + requestHandler: 'none', +}); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index c16847dab9dc2..a3c9d9d63e353 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -107,13 +107,13 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(await testSubjects.exists('headerGlobalNav')).to.be(true); }); - it('can navigate from NP apps to legacy apps', async () => { + it.skip('can navigate from NP apps to legacy apps', async () => { await appsMenu.clickLink('Management'); await loadingScreenShown(); await testSubjects.existOrFail('managementNav'); }); - it('can navigate from legacy apps to NP apps', async () => { + it.skip('can navigate from legacy apps to NP apps', async () => { await appsMenu.clickLink('Foo'); await loadingScreenShown(); await testSubjects.existOrFail('fooAppHome'); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx index a50d3371f47cf..d0b77a425d14a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx @@ -18,15 +18,18 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; -jest.mock('ui/new_platform'); -jest.mock('ui/chrome', () => ({ - getUiSettingsClient: () => ({ - get(path: string) { - if (path === 'histogram:maxBars') { - return 10; - } +jest.mock('ui/new_platform', () => ({ + npStart: { + core: { + uiSettings: { + get: (path: string) => { + if (path === 'histogram:maxBars') { + return 10; + } + }, + }, }, - }), + }, })); const defaultOptions = { diff --git a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts b/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts index 185df12054a3c..0c4e6d9f7cb10 100644 --- a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts +++ b/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { visualizations } from '../../../../../src/legacy/core_plugins/visualizations/public'; +import { setup as visualizations } from '../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/legacy'; import { getBasePath, getEditPath } from '../common'; visualizations.types.registerAlias({ From 8797e68db0b0499aebca3d4396a38b457fa97ef1 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 27 Nov 2019 12:50:19 +0200 Subject: [PATCH 109/128] Query String(Bar) Input - cleanup (#51598) * Moved Suggestions to NP Renamed QueryBarInput to QueryStringInput Changed IndexPattern to IIndexPattern * fix import * Update snapshot * css import * scss * eslint --- src/core/MIGRATION.md | 2 +- .../core_plugins/data/public/index.scss | 2 + src/legacy/core_plugins/data/public/index.ts | 2 +- ....snap => query_string_input.test.tsx.snap} | 18 ++--- .../query/query_bar/components/_index.scss | 1 - .../components/query_bar_top_row.test.tsx | 33 ++++------ .../components/query_bar_top_row.tsx | 8 +-- ...ks.ts => query_string_input.test.mocks.ts} | 20 ++---- ...t.test.tsx => query_string_input.test.tsx} | 65 ++++++++----------- ...y_bar_input.tsx => query_string_input.tsx} | 16 ++--- .../data/public/query/query_bar/index.ts | 2 +- .../search_bar/components/search_bar.test.tsx | 2 +- .../components/panel_config/gauge.test.js | 8 +-- .../public/components/query_bar_wrapper.js | 4 +- .../components/vis_types/gauge/series.test.js | 2 +- .../vis_types/metric/series.test.js | 2 +- .../vis/editors/default/controls/filter.tsx | 4 +- .../index_patterns/index_pattern.stub.ts | 15 +++++ src/plugins/data/public/stubs.ts | 2 +- src/plugins/data/public/ui/index.ts | 1 + .../suggestion_component.test.tsx.snap | 0 .../suggestions_component.test.tsx.snap | 0 .../data/public/ui}/typeahead/_index.scss | 0 .../public/ui}/typeahead/_suggestion.scss | 0 .../typeahead/suggestion_component.test.tsx | 2 +- .../ui}/typeahead/suggestion_component.tsx | 2 +- .../typeahead/suggestions_component.test.tsx | 2 +- .../ui}/typeahead/suggestions_component.tsx | 2 +- .../public/components/search_bar.test.tsx | 12 ++-- .../graph/public/components/search_bar.tsx | 7 +- 30 files changed, 110 insertions(+), 126 deletions(-) rename src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/{query_bar_input.test.tsx.snap => query_string_input.test.tsx.snap} (99%) rename src/legacy/core_plugins/data/public/query/query_bar/components/{query_bar_input.test.mocks.ts => query_string_input.test.mocks.ts} (83%) rename src/legacy/core_plugins/data/public/query/query_bar/components/{query_bar_input.test.tsx => query_string_input.test.tsx} (80%) rename src/legacy/core_plugins/data/public/query/query_bar/components/{query_bar_input.tsx => query_string_input.tsx} (97%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui}/typeahead/__snapshots__/suggestion_component.test.tsx.snap (100%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui}/typeahead/__snapshots__/suggestions_component.test.tsx.snap (100%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui}/typeahead/_index.scss (100%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui}/typeahead/_suggestion.scss (100%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui}/typeahead/suggestion_component.test.tsx (97%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui}/typeahead/suggestion_component.tsx (96%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui}/typeahead/suggestions_component.test.tsx (97%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui}/typeahead/suggestions_component.tsx (97%) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index c5e04c3cfb53a..e88f1675114bc 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1169,7 +1169,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `import 'ui/apply_filters'` | `import { applyFiltersPopover } from '../data/public'` | Directive is deprecated. | | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | -| `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | +| `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | | `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | diff --git a/src/legacy/core_plugins/data/public/index.scss b/src/legacy/core_plugins/data/public/index.scss index 913141666c7b9..94f02fe2d6049 100644 --- a/src/legacy/core_plugins/data/public/index.scss +++ b/src/legacy/core_plugins/data/public/index.scss @@ -4,4 +4,6 @@ @import 'src/plugins/data/public/ui/filter_bar/index'; +@import 'src/plugins/data/public/ui/typeahead/index'; + @import './search/search_bar/index'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 1349187779061..01f67a63ca9be 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -37,7 +37,7 @@ export { IndexPatterns, StaticIndexPattern, } from './index_patterns'; -export { QueryBarInput } from './query'; +export { QueryStringInput } from './query'; export { SearchBar, SearchBarProps, SavedQueryAttributes, SavedQuery } from './search'; /** @public static code */ diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_string_input.test.tsx.snap similarity index 99% rename from src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap rename to src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_string_input.test.tsx.snap index 5dc8702411783..6f155de95d6eb 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_bar_input.test.tsx.snap +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/query_string_input.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = ` +exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = ` -
    - + @@ -1114,7 +1114,7 @@ exports[`QueryBarInput Should disable autoFocus on EuiFieldText when disableAuto `; -exports[`QueryBarInput Should pass the query language to the language switcher 1`] = ` +exports[`QueryStringInput Should pass the query language to the language switcher 1`] = ` - - + @@ -2225,7 +2225,7 @@ exports[`QueryBarInput Should pass the query language to the language switcher 1 `; -exports[`QueryBarInput Should render the given query 1`] = ` +exports[`QueryStringInput Should render the given query 1`] = ` - - + diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/_index.scss b/src/legacy/core_plugins/data/public/query/query_bar/components/_index.scss index e17c416c13546..1d955920b8e13 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/_index.scss +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/_index.scss @@ -1,2 +1 @@ @import './query_bar'; -@import './typeahead/index'; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx index ae08083f82af3..ea01347e38865 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx @@ -17,12 +17,16 @@ * under the License. */ -import { mockPersistedLogFactory } from './query_bar_input.test.mocks'; +import { mockPersistedLogFactory } from './query_string_input.test.mocks'; import React from 'react'; import { mount } from 'enzyme'; import { QueryBarTopRow } from './query_bar_top_row'; -import { IndexPattern } from '../../../index'; + +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { stubIndexPatternWithFields } from '../../../../../../../plugins/data/public/stubs'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ import { coreMock } from '../../../../../../../core/public/mocks'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; @@ -85,21 +89,6 @@ const createMockStorage = () => ({ clear: jest.fn(), }); -const mockIndexPattern = { - id: '1234', - title: 'logstash-*', - fields: [ - { - name: 'response', - type: 'number', - esTypes: ['integer'], - aggregatable: true, - filterable: true, - searchable: true, - }, - ], -} as IndexPattern; - function wrapQueryBarTopRowInContext(testProps: any) { const defaultOptions = { screenTitle: 'Another Screen', @@ -124,7 +113,7 @@ function wrapQueryBarTopRowInContext(testProps: any) { } describe('QueryBarTopRowTopRow', () => { - const QUERY_INPUT_SELECTOR = 'QueryBarInputUI'; + const QUERY_INPUT_SELECTOR = 'QueryStringInputUI'; const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; const TIMEPICKER_DURATION = '[data-shared-timefilter-duration]'; @@ -138,7 +127,7 @@ describe('QueryBarTopRowTopRow', () => { query: kqlQuery, screenTitle: 'Another Screen', isDirty: false, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], timeHistory: mockTimeHistory, }) ); @@ -152,7 +141,7 @@ describe('QueryBarTopRowTopRow', () => { wrapQueryBarTopRowInContext({ query: kqlQuery, screenTitle: 'Another Screen', - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], timeHistory: mockTimeHistory, disableAutoFocus: true, isDirty: false, @@ -225,7 +214,7 @@ describe('QueryBarTopRowTopRow', () => { const component = mount( wrapQueryBarTopRowInContext({ query: kqlQuery, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], isDirty: false, screenTitle: 'Another Screen', showDatePicker: false, @@ -245,7 +234,7 @@ describe('QueryBarTopRowTopRow', () => { query: kqlQuery, isDirty: false, screenTitle: 'Another Screen', - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], showQueryInput: false, showDatePicker: false, timeHistory: mockTimeHistory, diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index ed3c2413b0eb4..824e8cf1e2a7c 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -34,6 +34,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; import { IDataPluginServices, + IIndexPattern, TimeRange, TimeHistoryContract, Query, @@ -42,8 +43,7 @@ import { esKuery, } from '../../../../../../../plugins/data/public'; import { useKibana, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; -import { IndexPattern } from '../../../index_patterns'; -import { QueryBarInput } from './query_bar_input'; +import { QueryStringInput } from './query_string_input'; interface Props { query?: Query; @@ -53,7 +53,7 @@ interface Props { dataTestSubj?: string; disableAutoFocus?: boolean; screenTitle?: string; - indexPatterns?: Array; + indexPatterns?: Array; intl: InjectedIntl; isLoading?: boolean; prepend?: React.ReactNode; @@ -178,7 +178,7 @@ function QueryBarTopRowUI(props: Props) { if (!shouldRenderQueryInput()) return; return ( - ({ PersistedLog: mockPersistedLogFactory, diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_string_input.test.tsx similarity index 80% rename from src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/query_string_input.test.tsx index 3edb689ca2bfe..3512604b36261 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_string_input.test.tsx @@ -21,15 +21,19 @@ import { mockFetchIndexPatterns, mockPersistedLog, mockPersistedLogFactory, -} from './query_bar_input.test.mocks'; +} from './query_string_input.test.mocks'; import { EuiFieldText } from '@elastic/eui'; import React from 'react'; import { QueryLanguageSwitcher } from './language_switcher'; -import { QueryBarInput, QueryBarInputUI } from './query_bar_input'; +import { QueryStringInput, QueryStringInputUI } from './query_string_input'; import { coreMock } from '../../../../../../../core/public/mocks'; const startMock = coreMock.createStart(); -import { IndexPattern } from '../../../index'; +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { stubIndexPatternWithFields } from '../../../../../../../plugins/data/public/stubs'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ + import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; import { mount } from 'enzyme'; @@ -65,22 +69,7 @@ const createMockStorage = () => ({ clear: jest.fn(), }); -const mockIndexPattern = { - id: '1234', - title: 'logstash-*', - fields: [ - { - name: 'response', - type: 'number', - esTypes: ['integer'], - aggregatable: true, - filterable: true, - searchable: true, - }, - ], -} as IndexPattern; - -function wrapQueryBarInputInContext(testProps: any, storage?: any) { +function wrapQueryStringInputInContext(testProps: any, storage?: any) { const defaultOptions = { screenTitle: 'Another Screen', intl: null as any, @@ -95,23 +84,23 @@ function wrapQueryBarInputInContext(testProps: any, storage?: any) { return ( - + ); } -describe('QueryBarInput', () => { +describe('QueryStringInput', () => { beforeEach(() => { jest.clearAllMocks(); }); it('Should render the given query', () => { const component = mount( - wrapQueryBarInputInContext({ + wrapQueryStringInputInContext({ query: kqlQuery, onSubmit: noop, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], }) ); @@ -120,10 +109,10 @@ describe('QueryBarInput', () => { it('Should pass the query language to the language switcher', () => { const component = mount( - wrapQueryBarInputInContext({ + wrapQueryStringInputInContext({ query: luceneQuery, onSubmit: noop, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], }) ); @@ -132,10 +121,10 @@ describe('QueryBarInput', () => { it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => { const component = mount( - wrapQueryBarInputInContext({ + wrapQueryStringInputInContext({ query: kqlQuery, onSubmit: noop, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], disableAutoFocus: true, }) ); @@ -147,10 +136,10 @@ describe('QueryBarInput', () => { mockPersistedLogFactory.mockClear(); mount( - wrapQueryBarInputInContext({ + wrapQueryStringInputInContext({ query: kqlQuery, onSubmit: noop, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], disableAutoFocus: true, appName: 'discover', }) @@ -162,11 +151,11 @@ describe('QueryBarInput', () => { const mockStorage = createMockStorage(); const mockCallback = jest.fn(); const component = mount( - wrapQueryBarInputInContext( + wrapQueryStringInputInContext( { query: kqlQuery, onSubmit: mockCallback, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], disableAutoFocus: true, appName: 'discover', }, @@ -186,15 +175,15 @@ describe('QueryBarInput', () => { const mockCallback = jest.fn(); const component = mount( - wrapQueryBarInputInContext({ + wrapQueryStringInputInContext({ query: kqlQuery, onSubmit: mockCallback, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], disableAutoFocus: true, }) ); - const instance = component.find('QueryBarInputUI').instance() as QueryBarInputUI; + const instance = component.find('QueryStringInputUI').instance() as QueryStringInputUI; const input = instance.inputRef; const inputWrapper = component.find(EuiFieldText).find('input'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); @@ -205,16 +194,16 @@ describe('QueryBarInput', () => { it('Should use PersistedLog for recent search suggestions', async () => { const component = mount( - wrapQueryBarInputInContext({ + wrapQueryStringInputInContext({ query: kqlQuery, onSubmit: noop, - indexPatterns: [mockIndexPattern], + indexPatterns: [stubIndexPatternWithFields], disableAutoFocus: true, persistedLog: mockPersistedLog, }) ); - const instance = component.find('QueryBarInputUI').instance() as QueryBarInputUI; + const instance = component.find('QueryStringInputUI').instance() as QueryStringInputUI; const input = instance.inputRef; const inputWrapper = component.find(EuiFieldText).find('input'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); @@ -229,7 +218,7 @@ describe('QueryBarInput', () => { it('Should accept index pattern strings and fetch the full object', () => { mockFetchIndexPatterns.mockClear(); mount( - wrapQueryBarInputInContext({ + wrapQueryStringInputInContext({ query: kqlQuery, onSubmit: noop, indexPatterns: ['logstash-*'], diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_string_input.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx rename to src/legacy/core_plugins/data/public/query/query_bar/components/query_string_input.tsx index dce245e0ccb24..37519551ac5ad 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_string_input.tsx @@ -38,7 +38,9 @@ import { AutocompleteSuggestion, AutocompleteSuggestionType, IDataPluginServices, + IIndexPattern, PersistedLog, + SuggestionsComponent, toUser, fromUser, matchPairs, @@ -50,15 +52,13 @@ import { KibanaReactContextValue, toMountPoint, } from '../../../../../../../plugins/kibana_react/public'; -import { IndexPattern, StaticIndexPattern } from '../../../index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; -import { SuggestionsComponent } from './typeahead/suggestions_component'; import { fetchIndexPatterns } from './fetch_index_patterns'; interface Props { kibana: KibanaReactContextValue; intl: InjectedIntl; - indexPatterns: Array; + indexPatterns: Array; query: Query; disableAutoFocus?: boolean; screenTitle?: string; @@ -79,7 +79,7 @@ interface State { suggestionLimit: number; selectionStart: number | null; selectionEnd: number | null; - indexPatterns: StaticIndexPattern[]; + indexPatterns: IIndexPattern[]; } const KEY_CODES = { @@ -96,7 +96,7 @@ const KEY_CODES = { const recentSearchType: AutocompleteSuggestionType = 'recentSearch'; -export class QueryBarInputUI extends Component { +export class QueryStringInputUI extends Component { public state: State = { isSuggestionsVisible: false, index: null, @@ -123,13 +123,13 @@ export class QueryBarInputUI extends Component { ) as string[]; const objectPatterns = this.props.indexPatterns.filter( indexPattern => typeof indexPattern !== 'string' - ) as IndexPattern[]; + ) as IIndexPattern[]; const objectPatternsFromStrings = (await fetchIndexPatterns( this.services.savedObjects!.client, stringPatterns, this.services.uiSettings! - )) as IndexPattern[]; + )) as IIndexPattern[]; this.setState({ indexPatterns: [...objectPatterns, ...objectPatternsFromStrings], @@ -589,4 +589,4 @@ export class QueryBarInputUI extends Component { } } -export const QueryBarInput = injectI18n(withKibana(QueryBarInputUI)); +export const QueryStringInput = injectI18n(withKibana(QueryStringInputUI)); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/index.ts b/src/legacy/core_plugins/data/public/query/query_bar/index.ts index f0ad0707c699a..47b0ca5eae1bf 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/index.ts +++ b/src/legacy/core_plugins/data/public/query/query_bar/index.ts @@ -18,4 +18,4 @@ */ export { QueryBarTopRow } from './components/query_bar_top_row'; -export { QueryBarInput } from './components/query_bar_input'; +export { QueryStringInput } from './components/query_string_input'; diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 0ca9482fefa30..da7008b579eb7 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -43,7 +43,7 @@ jest.mock('../../../../../../../plugins/data/public', () => { jest.mock('../../../../../data/public', () => { return { - QueryBarInput: () =>
    , + QueryStringInput: () =>
    , }; }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js index 7b8a7061e8bb1..9ec8184dbaebb 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.test.js @@ -22,18 +22,12 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; jest.mock('plugins/data', () => { return { - QueryBarInput: () =>
    , + QueryStringInput: () =>
    , }; }); import { GaugePanelConfig } from './gauge'; -jest.mock('plugins/data', () => { - return { - QueryBar: () =>
    , - }; -}); - describe('GaugePanelConfig', () => { it('call switch tab onChange={handleChange}', () => { const props = { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/query_bar_wrapper.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/query_bar_wrapper.js index 2eb9a7b03ac5f..dc976beeca0d1 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/query_bar_wrapper.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/query_bar_wrapper.js @@ -19,10 +19,10 @@ import React, { useContext } from 'react'; import { CoreStartContext } from '../contexts/query_input_bar_context'; -import { QueryBarInput } from 'plugins/data'; +import { QueryStringInput } from 'plugins/data'; export function QueryBarWrapper(props) { const coreStartContext = useContext(CoreStartContext); - return ; + return ; } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.test.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.test.js index edbeba5d176ae..4efd5bb65451c 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.test.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/gauge/series.test.js @@ -22,7 +22,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; jest.mock('plugins/data', () => { return { - QueryBarInput: () =>
    , + QueryStringInput: () =>
    , }; }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.test.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.test.js index cfcac5d4908a0..299e7c12f931a 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.test.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/metric/series.test.js @@ -23,7 +23,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; jest.mock('plugins/data', () => { return { - QueryBarInput: () =>
    , + QueryStringInput: () =>
    , }; }); diff --git a/src/legacy/ui/public/vis/editors/default/controls/filter.tsx b/src/legacy/ui/public/vis/editors/default/controls/filter.tsx index e847a95ead478..664a0b3e02a00 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/filter.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/filter.tsx @@ -20,7 +20,7 @@ import React, { useState } from 'react'; import { EuiForm, EuiButtonIcon, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { QueryBarInput } from 'plugins/data'; +import { QueryStringInput } from 'plugins/data'; import { Query } from 'src/plugins/data/public'; import { AggConfig } from '../../..'; import { npStart } from '../../../../new_platform'; @@ -100,7 +100,7 @@ function FilterRow({ ...npStart.core, }} > - onChangeValue(id, query, customLabel)} diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts index 3d5151752a080..4f8108575aa15 100644 --- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -26,3 +26,18 @@ export const stubIndexPattern: IIndexPattern = { title: 'logstash-*', timeFieldName: '@timestamp', }; + +export const stubIndexPatternWithFields: IIndexPattern = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], +}; diff --git a/src/plugins/data/public/stubs.ts b/src/plugins/data/public/stubs.ts index 01e68288bd655..d2519716dd83e 100644 --- a/src/plugins/data/public/stubs.ts +++ b/src/plugins/data/public/stubs.ts @@ -17,6 +17,6 @@ * under the License. */ -export { stubIndexPattern } from './index_patterns/index_pattern.stub'; +export { stubIndexPattern, stubIndexPatternWithFields } from './index_patterns/index_pattern.stub'; export { stubFields } from './index_patterns/field.stub'; export * from '../common/es_query/filters/stubs'; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index cb7c92b00ea3a..607f690d41c67 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,6 +17,7 @@ * under the License. */ +export { SuggestionsComponent } from './typeahead/suggestions_component'; export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { applyFiltersPopover } from './apply_filters'; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestion_component.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/__snapshots__/suggestion_component.test.tsx.snap rename to src/plugins/data/public/ui/typeahead/__snapshots__/suggestion_component.test.tsx.snap diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/__snapshots__/suggestions_component.test.tsx.snap rename to src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/_index.scss b/src/plugins/data/public/ui/typeahead/_index.scss similarity index 100% rename from src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/_index.scss rename to src/plugins/data/public/ui/typeahead/_index.scss diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/_suggestion.scss b/src/plugins/data/public/ui/typeahead/_suggestion.scss similarity index 100% rename from src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/_suggestion.scss rename to src/plugins/data/public/ui/typeahead/_suggestion.scss diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.test.tsx rename to src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx index dc7ebfc7b37ea..591176bf133fa 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx @@ -19,7 +19,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../../plugins/data/public'; +import { AutocompleteSuggestion } from '../..'; import { SuggestionComponent } from './suggestion_component'; const noop = () => { diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.tsx rename to src/plugins/data/public/ui/typeahead/suggestion_component.tsx index 27e3eb1eebd1b..fd29de4573ff0 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestion_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx @@ -20,7 +20,7 @@ import { EuiIcon } from '@elastic/eui'; import classNames from 'classnames'; import React, { FunctionComponent } from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../../plugins/data/public'; +import { AutocompleteSuggestion } from '../..'; function getEuiIconType(type: string) { switch (type) { diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.test.tsx rename to src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index ea360fc8fd72e..7fb2fdf25104a 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -19,7 +19,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../../plugins/data/public'; +import { AutocompleteSuggestion } from '../..'; import { SuggestionComponent } from './suggestion_component'; import { SuggestionsComponent } from './suggestions_component'; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx similarity index 97% rename from src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.tsx rename to src/plugins/data/public/ui/typeahead/suggestions_component.tsx index 32860e7cb390b..e4cccbcde4fb8 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -19,7 +19,7 @@ import { isEmpty } from 'lodash'; import React, { Component } from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../../plugins/data/public'; +import { AutocompleteSuggestion } from '../..'; import { SuggestionComponent } from './suggestion_component'; interface Props { diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx index fd2004558be77..a91e91258e240 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx @@ -9,7 +9,7 @@ import { SearchBar, OuterSearchBarProps } from './search_bar'; import React, { ReactElement } from 'react'; import { CoreStart } from 'src/core/public'; import { act } from 'react-dom/test-utils'; -import { QueryBarInput, IndexPattern } from 'src/legacy/core_plugins/data/public'; +import { QueryStringInput, IndexPattern } from 'src/legacy/core_plugins/data/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -25,7 +25,7 @@ import { Provider } from 'react-redux'; jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); jest.mock('../../../../../../src/legacy/core_plugins/data/public', () => ({ - QueryBarInput: () => null, + QueryStringInput: () => null, })); const waitForIndexPatternFetch = () => new Promise(r => setTimeout(r)); @@ -106,7 +106,7 @@ describe('search_bar', () => { await waitForIndexPatternFetch(); act(() => { - instance.find(QueryBarInput).prop('onChange')!({ language: 'lucene', query: 'testQuery' }); + instance.find(QueryStringInput).prop('onChange')!({ language: 'lucene', query: 'testQuery' }); }); act(() => { @@ -122,7 +122,7 @@ describe('search_bar', () => { await waitForIndexPatternFetch(); act(() => { - instance.find(QueryBarInput).prop('onChange')!({ language: 'kuery', query: 'test: abc' }); + instance.find(QueryStringInput).prop('onChange')!({ language: 'kuery', query: 'test: abc' }); }); act(() => { @@ -140,7 +140,9 @@ describe('search_bar', () => { // pick the button component out of the tree because // it's part of a popover and thus not covered by enzyme - (instance.find(QueryBarInput).prop('prepend') as ReactElement).props.children.props.onClick(); + (instance + .find(QueryStringInput) + .prop('prepend') as ReactElement).props.children.props.onClick(); expect(openSourceModal).toHaveBeenCalled(); }); diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx index 56458e5de273f..79ffad26cf981 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx @@ -10,7 +10,10 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { connect } from 'react-redux'; import { IndexPatternSavedObject, IndexPatternProvider } from '../types'; -import { QueryBarInput, IndexPattern } from '../../../../../../src/legacy/core_plugins/data/public'; +import { + QueryStringInput, + IndexPattern, +} from '../../../../../../src/legacy/core_plugins/data/public'; import { openSourceModal } from '../services/source_modal'; import { GraphState, @@ -101,7 +104,7 @@ export function SearchBarComponent(props: SearchBarProps) { > - Date: Wed, 27 Nov 2019 12:33:50 +0100 Subject: [PATCH 110/128] [Console] Proxy fallback (#50185) * First iteration of liveness manager for Console * First iteration of PoC working * Updated console proxy fallback behaviour after feedback * remove @types/node-fetch * If all hosts failed due to connection refused errors 502 * Remove unnecessary existence check --- src/legacy/core_plugins/console/index.ts | 2 +- .../server/__tests__/proxy_route/body.js | 2 +- .../server/__tests__/proxy_route/headers.js | 2 +- .../server/__tests__/proxy_route/params.js | 22 +---- .../__tests__/proxy_route/query_string.js | 2 +- .../server/{proxy_route.js => proxy_route.ts} | 94 ++++++++++++------- .../console/server/request.test.ts | 4 +- .../core_plugins/console/server/request.ts | 4 +- 8 files changed, 71 insertions(+), 61 deletions(-) rename src/legacy/core_plugins/console/server/{proxy_route.js => proxy_route.ts} (65%) diff --git a/src/legacy/core_plugins/console/index.ts b/src/legacy/core_plugins/console/index.ts index caef3ff6f99f3..c4e6a77b7d859 100644 --- a/src/legacy/core_plugins/console/index.ts +++ b/src/legacy/core_plugins/console/index.ts @@ -141,7 +141,7 @@ export default function(kibana: any) { server.route( createProxyRoute({ - baseUrl: head(legacyEsConfig.hosts), + hosts: legacyEsConfig.hosts, pathFilters: proxyPathFilters, getConfigForReq(req: any, uri: any) { const filteredHeaders = filterHeaders( diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/body.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/body.js index c9ad09cb017c4..a7fd8df1b10f4 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/body.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/body.js @@ -36,7 +36,7 @@ describe('Console Proxy Route', () => { const server = new Server(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], }) ); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/headers.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/headers.js index 2e78201f9990e..347b8dae80e29 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/headers.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/headers.js @@ -40,7 +40,7 @@ describe('Console Proxy Route', () => { const server = new Server(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], }) ); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js index aa7b764f84fc7..2cf09f96e7b72 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js @@ -72,7 +72,7 @@ describe('Console Proxy Route', () => { const { server } = setup(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], pathFilters: [/^\/foo\//, /^\/bar\//], }) ); @@ -91,7 +91,7 @@ describe('Console Proxy Route', () => { const { server } = setup(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], pathFilters: [/^\/foo\//, /^\/bar\//], }) ); @@ -113,7 +113,7 @@ describe('Console Proxy Route', () => { const getConfigForReq = sinon.stub().returns({}); - server.route(createProxyRoute({ baseUrl: 'http://localhost:9200', getConfigForReq })); + server.route(createProxyRoute({ hosts: ['http://localhost:9200'], getConfigForReq })); await server.inject({ method: 'POST', url: '/api/console/proxy?method=HEAD&path=/index/id', @@ -142,7 +142,7 @@ describe('Console Proxy Route', () => { server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], getConfigForReq: () => ({ timeout, agent, @@ -166,19 +166,5 @@ describe('Console Proxy Route', () => { expect(opts.headers).to.have.property('baz', 'bop'); }); }); - - describe('baseUrl', () => { - describe('default', () => { - it('ensures that the path starts with a /'); - }); - describe('url ends with a slash', () => { - it('combines clean with paths that start with a slash'); - it(`combines clean with paths that don't start with a slash`); - }); - describe(`url doesn't end with a slash`, () => { - it('combines clean with paths that start with a slash'); - it(`combines clean with paths that don't start with a slash`); - }); - }); }); }); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js index f20adb897be65..6b98702131d91 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js @@ -38,7 +38,7 @@ describe('Console Proxy Route', () => { const server = new Server(); server.route( createProxyRoute({ - baseUrl: 'http://localhost:9200', + hosts: ['http://localhost:9200'], }) ); diff --git a/src/legacy/core_plugins/console/server/proxy_route.js b/src/legacy/core_plugins/console/server/proxy_route.ts similarity index 65% rename from src/legacy/core_plugins/console/server/proxy_route.js rename to src/legacy/core_plugins/console/server/proxy_route.ts index 856128f3d4c03..f67c97443ba07 100644 --- a/src/legacy/core_plugins/console/server/proxy_route.js +++ b/src/legacy/core_plugins/console/server/proxy_route.ts @@ -18,12 +18,13 @@ */ import Joi from 'joi'; +import * as url from 'url'; +import { IncomingMessage } from 'http'; import Boom from 'boom'; import { trimLeft, trimRight } from 'lodash'; import { sendRequest } from './request'; -import * as url from 'url'; -function toURL(base, path) { +function toURL(base: string, path: string) { const urlResult = new url.URL(`${trimRight(base, '/')}/${trimLeft(path, '/')}`); // Appending pretty here to have Elasticsearch do the JSON formatting, as doing // in JS can lead to data loss (7.0 will get munged into 7, thus losing indication of @@ -34,11 +35,11 @@ function toURL(base, path) { return urlResult; } -function getProxyHeaders(req) { +function getProxyHeaders(req: any) { const headers = Object.create(null); // Scope this proto-unsafe functionality to where it is being used. - function extendCommaList(obj, property, value) { + function extendCommaList(obj: Record, property: string, value: any) { obj[property] = (obj[property] ? obj[property] + ',' : '') + value; } @@ -58,9 +59,13 @@ function getProxyHeaders(req) { } export const createProxyRoute = ({ - baseUrl = '/', + hosts, pathFilters = [/.*/], getConfigForReq = () => ({}), +}: { + hosts: string[]; + pathFilters: RegExp[]; + getConfigForReq: (...args: any[]) => any; }) => ({ path: '/api/console/proxy', method: 'POST', @@ -84,7 +89,7 @@ export const createProxyRoute = ({ }, pre: [ - function filterPath(req) { + function filterPath(req: any) { const { path } = req.query; if (pathFilters.some(re => re.test(path))) { @@ -92,55 +97,74 @@ export const createProxyRoute = ({ } const err = Boom.forbidden(); - err.output.payload = `Error connecting to '${path}':\n\nUnable to send requests to that path.`; + err.output.payload = `Error connecting to '${path}':\n\nUnable to send requests to that path.` as any; err.output.headers['content-type'] = 'text/plain'; throw err; }, ], - handler: async (req, h) => { + handler: async (req: any, h: any) => { const { payload, query } = req; const { path, method } = query; - const uri = toURL(baseUrl, path); - - // Because this can technically be provided by a settings-defined proxy config, we need to - // preserve these property names to maintain BWC. - const { timeout, agent, headers, rejectUnauthorized } = getConfigForReq(req, uri.toString()); - - const requestHeaders = { - ...headers, - ...getProxyHeaders(req), - }; - - const esIncomingMessage = await sendRequest({ - method, - headers: requestHeaders, - uri, - timeout, - payload, - rejectUnauthorized, - agent, - }); + + let esIncomingMessage: IncomingMessage; + + for (let idx = 0; idx < hosts.length; ++idx) { + const host = hosts[idx]; + try { + const uri = toURL(host, path); + + // Because this can technically be provided by a settings-defined proxy config, we need to + // preserve these property names to maintain BWC. + const { timeout, agent, headers, rejectUnauthorized } = getConfigForReq( + req, + uri.toString() + ); + + const requestHeaders = { + ...headers, + ...getProxyHeaders(req), + }; + + esIncomingMessage = await sendRequest({ + method, + headers: requestHeaders, + uri, + timeout, + payload, + rejectUnauthorized, + agent, + }); + + break; + } catch (e) { + if (e.code !== 'ECONNREFUSED') { + throw Boom.boomify(e); + } + if (idx === hosts.length - 1) { + throw Boom.badGateway('Could not reach any configured nodes.'); + } + // Otherwise, try the next host... + } + } const { statusCode, statusMessage, - headers: responseHeaders, - } = esIncomingMessage; - - const { warning } = responseHeaders; + headers: { warning }, + } = esIncomingMessage!; if (method.toUpperCase() !== 'HEAD') { return h - .response(esIncomingMessage) + .response(esIncomingMessage!) .code(statusCode) - .header('warning', warning); + .header('warning', warning!); } else { return h .response(`${statusCode} - ${statusMessage}`) .code(statusCode) .type('text/plain') - .header('warning', warning); + .header('warning', warning!); } }, }, diff --git a/src/legacy/core_plugins/console/server/request.test.ts b/src/legacy/core_plugins/console/server/request.test.ts index d5504c0f3a3c2..2cbde5b3b39b8 100644 --- a/src/legacy/core_plugins/console/server/request.test.ts +++ b/src/legacy/core_plugins/console/server/request.test.ts @@ -24,7 +24,7 @@ import { fail } from 'assert'; describe(`Console's send request`, () => { let sandbox: sinon.SinonSandbox; - let stub: sinon.SinonStub, ClientRequest>; + let stub: sinon.SinonStub, ClientRequest>; let fakeRequest: http.ClientRequest; beforeEach(() => { @@ -52,7 +52,7 @@ describe(`Console's send request`, () => { method: 'get', payload: null as any, timeout: 0, // immediately timeout - uri: new URL('http://noone.nowhere.com'), + uri: new URL('http://noone.nowhere.none'), }); fail('Should not reach here!'); } catch (e) { diff --git a/src/legacy/core_plugins/console/server/request.ts b/src/legacy/core_plugins/console/server/request.ts index 0082f3591a132..0f6b78b484adf 100644 --- a/src/legacy/core_plugins/console/server/request.ts +++ b/src/legacy/core_plugins/console/server/request.ts @@ -89,7 +89,7 @@ export const sendRequest = ({ } }); - const onError = () => reject(); + const onError = (e: Error) => reject(e); req.once('error', onError); const timeoutPromise = new Promise((timeoutResolve, timeoutReject) => { @@ -103,5 +103,5 @@ export const sendRequest = ({ }, timeout); }); - return Promise.race([reqPromise, timeoutPromise]); + return Promise.race([reqPromise, timeoutPromise]); }; From c790ef277cdb9f6b0e382800ca289c9c565963c8 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Wed, 27 Nov 2019 14:17:52 +0100 Subject: [PATCH 111/128] fixes drag and drop in tests (#51806) --- .../cypress/integration/lib/drag_n_drop/helpers.ts | 10 +++++++--- .../smoke_tests/fields_browser/fields_browser.spec.ts | 2 +- .../smoke_tests/timeline/toggle_column.spec.ts | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/drag_n_drop/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/drag_n_drop/helpers.ts index e42a01f4ad8c1..39a61401c15b3 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/drag_n_drop/helpers.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/drag_n_drop/helpers.ts @@ -23,19 +23,23 @@ export const drag = (subject: JQuery) => { clientY: subjectLocation.top, force: true, }) + .wait(1) .trigger('mousemove', { button: primaryButton, clientX: subjectLocation.left + dndSloppyClickDetectionThreshold, clientY: subjectLocation.top, force: true, - }); + }) + .wait(1); }; /** "Drops" the subject being dragged on the specified drop target */ export const drop = (dropTarget: JQuery) => { cy.wrap(dropTarget) - .trigger('mousemove', { button: primaryButton }) - .trigger('mouseup'); + .trigger('mousemove', { button: primaryButton, force: true }) + .wait(1) + .trigger('mouseup', { force: true }) + .wait(1); }; /** Drags the subject being dragged on the specified drop target, but does not drop it */ diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts index 2d613ab09f1c1..8f5c6e6f660cc 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts @@ -196,7 +196,7 @@ describe('Fields Browser', () => { ); }); - it.skip('adds a field to the timeline when the user drags and drops a field', () => { + it('adds a field to the timeline when the user drags and drops a field', () => { const filterInput = 'host.geo.c'; const toggleField = 'host.geo.city_name'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts index 8c2902fd804ac..8197f77db9a08 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/toggle_column.spec.ts @@ -73,7 +73,7 @@ describe('toggle column in timeline', () => { cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${idField}"]`).should('exist'); }); - it.skip('adds the _id field to the timeline via drag and drop', () => { + it('adds the _id field to the timeline via drag and drop', () => { populateTimeline(); toggleFirstTimelineEventDetails(); From 7830946f6a1644701fc6bbb085337007afa6c526 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 27 Nov 2019 09:26:21 -0500 Subject: [PATCH 112/128] Fix infinite redirect loop when multiple cookies are sent (#50452) Cookies are now checked for attributes that match the current Kibana configuration. Invalid cookies are cleared more reliably. --- .../core/server/kibana-plugin-server.md | 1 + ...r.sessioncookievalidationresult.isvalid.md | 13 ++ ...in-server.sessioncookievalidationresult.md | 21 ++ ...rver.sessioncookievalidationresult.path.md | 13 ++ ...ssionstoragecookieoptions.encryptionkey.md | 2 +- ...ugin-server.sessionstoragecookieoptions.md | 4 +- ...er.sessionstoragecookieoptions.validate.md | 4 +- .../server/http/cookie_session_storage.ts | 41 +++- .../server/http/cookie_sesson_storage.test.ts | 71 +++++- src/core/server/http/http_server.test.ts | 2 +- src/core/server/http/index.ts | 5 +- .../integration_tests/core_services.test.ts | 2 +- .../http/integration_tests/lifecycle.test.ts | 2 +- src/core/server/index.ts | 1 + src/core/server/server.api.md | 8 +- .../authentication/authenticator.test.ts | 205 +++++------------- .../server/authentication/authenticator.ts | 13 ++ .../security/server/authentication/index.ts | 30 ++- 18 files changed, 256 insertions(+), 182 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.isvalid.md create mode 100644 docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.md create mode 100644 docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.path.md diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 360675b3490c2..13e0ea3645f26 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -115,6 +115,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | | [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | | +| [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) | Return type from a function to validate cookie contents. | | [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | | [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. | | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | diff --git a/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.isvalid.md b/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.isvalid.md new file mode 100644 index 0000000000000..6e5f6acca2eb9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.isvalid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) > [isValid](./kibana-plugin-server.sessioncookievalidationresult.isvalid.md) + +## SessionCookieValidationResult.isValid property + +Whether the cookie is valid or not. + +Signature: + +```typescript +isValid: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.md b/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.md new file mode 100644 index 0000000000000..6d32c4cca3dd6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) + +## SessionCookieValidationResult interface + +Return type from a function to validate cookie contents. + +Signature: + +```typescript +export interface SessionCookieValidationResult +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [isValid](./kibana-plugin-server.sessioncookievalidationresult.isvalid.md) | boolean | Whether the cookie is valid or not. | +| [path](./kibana-plugin-server.sessioncookievalidationresult.path.md) | string | The "Path" attribute of the cookie; if the cookie is invalid, this is used to clear it. | + diff --git a/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.path.md b/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.path.md new file mode 100644 index 0000000000000..8ca6d452213aa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sessioncookievalidationresult.path.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) > [path](./kibana-plugin-server.sessioncookievalidationresult.path.md) + +## SessionCookieValidationResult.path property + +The "Path" attribute of the cookie; if the cookie is invalid, this is used to clear it. + +Signature: + +```typescript +path?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md index 167ab03d7567f..ef65735e7bdba 100644 --- a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md @@ -4,7 +4,7 @@ ## SessionStorageCookieOptions.encryptionKey property -A key used to encrypt a cookie value. Should be at least 32 characters long. +A key used to encrypt a cookie's value. Should be at least 32 characters long. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md index de412818142f2..778dc27a190d9 100644 --- a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.md @@ -16,8 +16,8 @@ export interface SessionStorageCookieOptions | Property | Type | Description | | --- | --- | --- | -| [encryptionKey](./kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md) | string | A key used to encrypt a cookie value. Should be at least 32 characters long. | +| [encryptionKey](./kibana-plugin-server.sessionstoragecookieoptions.encryptionkey.md) | string | A key used to encrypt a cookie's value. Should be at least 32 characters long. | | [isSecure](./kibana-plugin-server.sessionstoragecookieoptions.issecure.md) | boolean | Flag indicating whether the cookie should be sent only via a secure connection. | | [name](./kibana-plugin-server.sessionstoragecookieoptions.name.md) | string | Name of the session cookie. | -| [validate](./kibana-plugin-server.sessionstoragecookieoptions.validate.md) | (sessionValue: T) => boolean | Promise<boolean> | Function called to validate a cookie content. | +| [validate](./kibana-plugin-server.sessionstoragecookieoptions.validate.md) | (sessionValue: T | T[]) => SessionCookieValidationResult | Function called to validate a cookie's decrypted value. | diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md index f3cbfc0d84e18..effa4b6bbc077 100644 --- a/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragecookieoptions.validate.md @@ -4,10 +4,10 @@ ## SessionStorageCookieOptions.validate property -Function called to validate a cookie content. +Function called to validate a cookie's decrypted value. Signature: ```typescript -validate: (sessionValue: T) => boolean | Promise; +validate: (sessionValue: T | T[]) => SessionCookieValidationResult; ``` diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 8a1b56d87fb4c..25b463140bfbc 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -34,19 +34,34 @@ export interface SessionStorageCookieOptions { */ name: string; /** - * A key used to encrypt a cookie value. Should be at least 32 characters long. + * A key used to encrypt a cookie's value. Should be at least 32 characters long. */ encryptionKey: string; /** - * Function called to validate a cookie content. + * Function called to validate a cookie's decrypted value. */ - validate: (sessionValue: T) => boolean | Promise; + validate: (sessionValue: T | T[]) => SessionCookieValidationResult; /** * Flag indicating whether the cookie should be sent only via a secure connection. */ isSecure: boolean; } +/** + * Return type from a function to validate cookie contents. + * @public + */ +export interface SessionCookieValidationResult { + /** + * Whether the cookie is valid or not. + */ + isValid: boolean; + /** + * The "Path" attribute of the cookie; if the cookie is invalid, this is used to clear it. + */ + path?: string; +} + class ScopedCookieSessionStorage> implements SessionStorage { constructor( private readonly log: Logger, @@ -98,15 +113,31 @@ export async function createCookieSessionStorageFactory( cookieOptions: SessionStorageCookieOptions, basePath?: string ): Promise> { + function clearInvalidCookie(req: Request | undefined, path: string = basePath || '/') { + // if the cookie did not include the 'path' attribute in the session value, it is a legacy cookie + // we will assume that the cookie was created with the current configuration + log.debug(`Clearing invalid session cookie`); + // need to use Hapi toolkit to clear cookie with defined options + if (req) { + (req.cookieAuth as any).h.unstate(cookieOptions.name, { path }); + } + } + await server.register({ plugin: hapiAuthCookie }); server.auth.strategy('security-cookie', 'cookie', { cookie: cookieOptions.name, password: cookieOptions.encryptionKey, - validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }), + validateFunc: async (req, session: T | T[]) => { + const result = cookieOptions.validate(session); + if (!result.isValid) { + clearInvalidCookie(req, result.path); + } + return { valid: result.isValid }; + }, isSecure: cookieOptions.isSecure, path: basePath, - clearInvalid: true, + clearInvalid: false, isHttpOnly: true, isSameSite: false, }); diff --git a/src/core/server/http/cookie_sesson_storage.test.ts b/src/core/server/http/cookie_sesson_storage.test.ts index 5cd2fbaa1ebe8..bf0585ad280d5 100644 --- a/src/core/server/http/cookie_sesson_storage.test.ts +++ b/src/core/server/http/cookie_sesson_storage.test.ts @@ -80,6 +80,7 @@ interface User { interface Storage { value: User; expires: number; + path: string; } function retrieveSessionCookie(cookies: string) { @@ -92,13 +93,21 @@ function retrieveSessionCookie(cookies: string) { const userData = { id: '42' }; const sessionDurationMs = 1000; +const path = '/'; +const sessVal = () => ({ value: userData, expires: Date.now() + sessionDurationMs, path }); const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); const cookieOptions = { name: 'sid', encryptionKey: 'something_at_least_32_characters', - validate: (session: Storage) => session.expires > Date.now(), + validate: (session: Storage | Storage[]) => { + if (Array.isArray(session)) { + session = session[0]; + } + const isValid = session.path === path && session.expires > Date.now(); + return { isValid, path: session.path }; + }, isSecure: false, - path: '/', + path, }; describe('Cookie based SessionStorage', () => { @@ -107,9 +116,9 @@ describe('Cookie based SessionStorage', () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter(''); - router.get({ path: '/', validate: false }, (context, req, res) => { + router.get({ path, validate: false }, (context, req, res) => { const sessionStorage = factory.asScoped(req); - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + sessionStorage.set(sessVal()); return res.ok({}); }); @@ -136,6 +145,7 @@ describe('Cookie based SessionStorage', () => { expect(sessionCookie.httpOnly).toBe(true); }); }); + describe('#get()', () => { it('reads from session storage', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); @@ -145,7 +155,7 @@ describe('Cookie based SessionStorage', () => { const sessionStorage = factory.asScoped(req); const sessionValue = await sessionStorage.get(); if (!sessionValue) { - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + sessionStorage.set(sessVal()); return res.ok(); } return res.ok({ body: { value: sessionValue.value } }); @@ -173,6 +183,7 @@ describe('Cookie based SessionStorage', () => { .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) .expect(200, { value: userData }); }); + it('returns null for empty session', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); @@ -198,7 +209,7 @@ describe('Cookie based SessionStorage', () => { expect(cookies).not.toBeDefined(); }); - it('returns null for invalid session & clean cookies', async () => { + it('returns null for invalid session (expired) & clean cookies', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter(''); @@ -208,7 +219,7 @@ describe('Cookie based SessionStorage', () => { const sessionStorage = factory.asScoped(req); if (!setOnce) { setOnce = true; - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + sessionStorage.set(sessVal()); return res.ok({ body: { value: userData } }); } const sessionValue = await sessionStorage.get(); @@ -242,6 +253,50 @@ describe('Cookie based SessionStorage', () => { 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', ]); }); + + it('returns null for invalid session (incorrect path) & clean cookies accurately', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + + const router = createRouter(''); + + let setOnce = false; + router.get({ path: '/', validate: false }, async (context, req, res) => { + const sessionStorage = factory.asScoped(req); + if (!setOnce) { + setOnce = true; + sessionStorage.set({ ...sessVal(), path: '/foo' }); + return res.ok({ body: { value: userData } }); + } + const sessionValue = await sessionStorage.get(); + return res.ok({ body: { value: sessionValue } }); + }); + + const factory = await createCookieSessionStorageFactory( + logger.get(), + innerServer, + cookieOptions + ); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200, { value: userData }); + + const cookies = response.get('set-cookie'); + expect(cookies).toBeDefined(); + + const sessionCookie = retrieveSessionCookie(cookies[0]); + const response2 = await supertest(innerServer.listener) + .get('/') + .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) + .expect(200, { value: null }); + + const cookies2 = response2.get('set-cookie'); + expect(cookies2).toEqual([ + 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/foo', + ]); + }); + // use mocks to simplify test setup it('returns null if multiple session cookies are detected.', async () => { const mockServer = { @@ -342,7 +397,7 @@ describe('Cookie based SessionStorage', () => { sessionStorage.clear(); return res.ok({}); } - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + sessionStorage.set(sessVal()); return res.ok({}); }); diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index acae9d8ff0e70..ceecfcfea1449 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -34,7 +34,7 @@ import { HttpServer } from './http_server'; const cookieOptions = { name: 'sid', encryptionKey: 'something_at_least_32_characters', - validate: () => true, + validate: () => ({ isValid: true }), isSecure: false, }; diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 2fa67750f6406..bed76201bb4f9 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -60,6 +60,9 @@ export { } from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; export { SessionStorageFactory, SessionStorage } from './session_storage'; -export { SessionStorageCookieOptions } from './cookie_session_storage'; +export { + SessionStorageCookieOptions, + SessionCookieValidationResult, +} from './cookie_session_storage'; export * from './types'; export { BasePath, IBasePath } from './base_path_service'; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 00629b811b28f..f3867faa2ae75 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -39,7 +39,7 @@ describe('http service', () => { const cookieOptions = { name: 'sid', encryptionKey: 'something_at_least_32_characters', - validate: (session: StorageData) => true, + validate: () => ({ isValid: true }), isSecure: false, path: '/', }; diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 4592a646b7f04..7c4a0097456ca 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -408,7 +408,7 @@ describe('Auth', () => { const cookieOptions = { name: 'sid', encryptionKey: 'something_at_least_32_characters', - validate: () => true, + validate: () => ({ isValid: true }), isSecure: false, }; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 31dec2c9b96ff..b53f04d601ff4 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -117,6 +117,7 @@ export { RouteRegistrar, SessionStorage, SessionStorageCookieOptions, + SessionCookieValidationResult, SessionStorageFactory, } from './http'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index d6cfa54397565..3bbcb85fea9e5 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1577,6 +1577,12 @@ export class ScopedClusterClient implements IScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise; } +// @public +export interface SessionCookieValidationResult { + isValid: boolean; + path?: string; +} + // @public export interface SessionStorage { clear(): void; @@ -1589,7 +1595,7 @@ export interface SessionStorageCookieOptions { encryptionKey: string; isSecure: boolean; name: string; - validate: (sessionValue: T) => boolean | Promise; + validate: (sessionValue: T | T[]) => SessionCookieValidationResult; } // @public diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 12b4620d554a2..1ba98d58a3a5f 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -78,12 +78,20 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; + let mockSessVal: any; beforeEach(() => { mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} }, }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockSessVal = { + idleTimeoutExpiration: null, + lifespanExpiration: null, + state: { authorization: 'Basic xxx' }, + provider: 'basic', + path: mockOptions.basePath.serverBasePath, + }; authenticator = new Authenticator(mockOptions); }); @@ -159,10 +167,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - idleTimeoutExpiration: null, - lifespanExpiration: null, + ...mockSessVal, state: { authorization }, - provider: 'basic', }); }); @@ -176,18 +182,12 @@ describe('Authenticator', () => { }); it('clears session if it belongs to a different provider.', async () => { - const state = { authorization: 'Basic xxx' }; const user = mockAuthenticatedUser(); const credentials = { username: 'user', password: 'password' }; const request = httpServerMock.createKibanaRequest(); mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user)); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'token', - }); + mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); const authenticationResult = await authenticator.login(request, { provider: 'basic', @@ -299,12 +299,20 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; + let mockSessVal: any; beforeEach(() => { mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} }, }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockSessVal = { + idleTimeoutExpiration: null, + lifespanExpiration: null, + state: { authorization: 'Basic xxx' }, + provider: 'basic', + path: mockOptions.basePath.serverBasePath, + }; authenticator = new Authenticator(mockOptions); }); @@ -360,10 +368,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - idleTimeoutExpiration: null, - lifespanExpiration: null, + ...mockSessVal, state: { authorization }, - provider: 'basic', }); }); @@ -383,28 +389,20 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - idleTimeoutExpiration: null, - lifespanExpiration: null, + ...mockSessVal, state: { authorization }, - provider: 'basic', }); }); it('does not extend session for system API calls.', async () => { const user = mockAuthenticatedUser(); - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -416,37 +414,25 @@ describe('Authenticator', () => { it('extends session for non-system API calls.', async () => { const user = mockAuthenticatedUser(); - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); expect(authenticationResult.user).toEqual(user); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); - expect(mockSessionStorage.set).toHaveBeenCalledWith({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + expect(mockSessionStorage.set).toHaveBeenCalledWith(mockSessVal); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); it('properly extends session expiration if it is defined.', async () => { const user = mockAuthenticatedUser(); - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); @@ -460,12 +446,7 @@ describe('Authenticator', () => { }); mockSessionStorage = sessionStorageMock.create(); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); authenticator = new Authenticator(mockOptions); @@ -482,17 +463,14 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ + ...mockSessVal, idleTimeoutExpiration: currentDate + 3600 * 24, - lifespanExpiration: null, - state, - provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); it('does not extend session lifespan expiration.', async () => { const user = mockAuthenticatedUser(); - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); const hr = 1000 * 60 * 60; @@ -508,12 +486,11 @@ describe('Authenticator', () => { mockSessionStorage = sessionStorageMock.create(); mockSessionStorage.get.mockResolvedValue({ + ...mockSessVal, // this session was created 6.5 hrs ago (and has 1.5 hrs left in its lifespan) // it was last extended 1 hour ago, which means it will expire in 1 hour idleTimeoutExpiration: currentDate + hr * 1, lifespanExpiration: currentDate + hr * 1.5, - state, - provider: 'basic', }); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -531,17 +508,15 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ + ...mockSessVal, idleTimeoutExpiration: currentDate + hr * 2, lifespanExpiration: currentDate + hr * 1.5, - state, - provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); it('only updates the session lifespan expiration if it does not match the current server config.', async () => { const user = mockAuthenticatedUser(); - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); const hr = 1000 * 60 * 60; @@ -560,10 +535,9 @@ describe('Authenticator', () => { mockSessionStorage = sessionStorageMock.create(); mockSessionStorage.get.mockResolvedValue({ + ...mockSessVal, idleTimeoutExpiration: 1, lifespanExpiration: oldExpiration, - state, - provider: 'basic', }); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -579,10 +553,9 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ + ...mockSessVal, idleTimeoutExpiration: 1, lifespanExpiration: newExpiration, - state, - provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); } @@ -595,19 +568,13 @@ describe('Authenticator', () => { }); it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -617,19 +584,13 @@ describe('Authenticator', () => { }); it('does not touch session for non-system API calls if authentication fails with non-401 reason.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -640,7 +601,6 @@ describe('Authenticator', () => { it('replaces existing session with the one returned by authentication provider for system API requests', async () => { const user = mockAuthenticatedUser(); - const existingState = { authorization: 'Basic xxx' }; const newState = { authorization: 'Basic yyy' }; const request = httpServerMock.createKibanaRequest(); @@ -648,12 +608,7 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: newState }) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state: existingState, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -661,17 +616,14 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - idleTimeoutExpiration: null, - lifespanExpiration: null, + ...mockSessVal, state: newState, - provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); it('replaces existing session with the one returned by authentication provider for non-system API requests', async () => { const user = mockAuthenticatedUser(); - const existingState = { authorization: 'Basic xxx' }; const newState = { authorization: 'Basic yyy' }; const request = httpServerMock.createKibanaRequest(); @@ -679,12 +631,7 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: newState }) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state: existingState, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -692,28 +639,20 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - idleTimeoutExpiration: null, - lifespanExpiration: null, + ...mockSessVal, state: newState, - provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); it('clears session if provider failed to authenticate system API request with 401 with active session.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -723,19 +662,13 @@ describe('Authenticator', () => { }); it('clears session if provider failed to authenticate non-system API request with 401 with active session.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -745,18 +678,12 @@ describe('Authenticator', () => { }); it('clears session if provider requested it via setting state to `null`.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.redirectTo('some-url', { state: null }) ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.redirected()).toBe(true); @@ -766,19 +693,13 @@ describe('Authenticator', () => { }); it('does not clear session if provider can not handle system API request authentication with active session.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -788,19 +709,13 @@ describe('Authenticator', () => { }); it('does not clear session if provider can not handle non-system API request authentication with active session.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -810,19 +725,13 @@ describe('Authenticator', () => { }); it('clears session for system API request if it belongs to not configured provider.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'token', - }); + mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -832,19 +741,13 @@ describe('Authenticator', () => { }); it('clears session for non-system API request if it belongs to not configured provider.', async () => { - const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'token', - }); + mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -858,12 +761,20 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; + let mockSessVal: any; beforeEach(() => { mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} }, }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockSessVal = { + idleTimeoutExpiration: null, + lifespanExpiration: null, + state: { authorization: 'Basic xxx' }, + provider: 'basic', + path: mockOptions.basePath.serverBasePath, + }; authenticator = new Authenticator(mockOptions); }); @@ -886,16 +797,10 @@ describe('Authenticator', () => { it('clears session and returns whatever authentication provider returns.', async () => { const request = httpServerMock.createKibanaRequest(); - const state = { authorization: 'Basic xxx' }; mockBasicAuthenticationProvider.logout.mockResolvedValue( DeauthenticationResult.redirectTo('some-url') ); - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'basic', - }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); const deauthenticationResult = await authenticator.logout(request); @@ -934,12 +839,7 @@ describe('Authenticator', () => { it('only clears session if it belongs to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); const state = { authorization: 'Bearer xxx' }; - mockSessionStorage.get.mockResolvedValue({ - idleTimeoutExpiration: null, - lifespanExpiration: null, - state, - provider: 'token', - }); + mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, state, provider: 'token' }); const deauthenticationResult = await authenticator.logout(request); @@ -978,6 +878,7 @@ describe('Authenticator', () => { lifespanExpiration: mockInfo.lifespanExpiration, state, provider: mockInfo.provider, + path: mockOptions.basePath.serverBasePath, }); jest.spyOn(Date, 'now').mockImplementation(() => currentDate); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 17a773c6b6e8c..8f947349cb2e8 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -59,6 +59,11 @@ export interface ProviderSession { * entirely determined by the authentication provider that owns the current session. */ state: unknown; + + /** + * Cookie "Path" attribute that is validated against the current Kibana server configuration. + */ + path: string; } /** @@ -159,6 +164,11 @@ export class Authenticator { */ private readonly providers: Map; + /** + * Which base path the HTTP server is hosted on. + */ + private readonly serverBasePath: string; + /** * Session timeout in ms. If `null` session will stay active until the browser is closed. */ @@ -213,6 +223,7 @@ export class Authenticator { ] as [string, BaseAuthenticationProvider]; }) ); + this.serverBasePath = this.options.basePath.serverBasePath || '/'; // only set these vars if they are defined in options (otherwise coalesce to existing/default) this.idleTimeout = this.options.config.session.idleTimeout; @@ -277,6 +288,7 @@ export class Authenticator { provider: attempt.provider, idleTimeoutExpiration, lifespanExpiration, + path: this.serverBasePath, }); } @@ -465,6 +477,7 @@ export class Authenticator { provider: providerType, idleTimeoutExpiration, lifespanExpiration, + path: this.serverBasePath, }); } } diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 2e67a0eaaa6d5..de2fb54ab8c2a 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -65,6 +65,22 @@ export async function setupAuthentication({ .callAsCurrentUser('shield.authenticate')) as AuthenticatedUser; }; + const isValid = (sessionValue: ProviderSession) => { + // ensure that this cookie was created with the current Kibana configuration + const { path, idleTimeoutExpiration, lifespanExpiration } = sessionValue; + if (path !== undefined && path !== (http.basePath.serverBasePath || '/')) { + authLogger.debug(`Outdated session value with path "${sessionValue.path}"`); + return false; + } + // ensure that this cookie is not expired + if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) { + return false; + } else if (lifespanExpiration && lifespanExpiration < Date.now()) { + return false; + } + return true; + }; + const authenticator = new Authenticator({ clusterClient, basePath: http.basePath, @@ -75,14 +91,14 @@ export async function setupAuthentication({ encryptionKey: config.encryptionKey, isSecure: config.secureCookies, name: config.cookieName, - validate: (sessionValue: ProviderSession) => { - const { idleTimeoutExpiration, lifespanExpiration } = sessionValue; - if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) { - return false; - } else if (lifespanExpiration && lifespanExpiration < Date.now()) { - return false; + validate: (session: ProviderSession | ProviderSession[]) => { + const array: ProviderSession[] = Array.isArray(session) ? session : [session]; + for (const sess of array) { + if (!isValid(sess)) { + return { isValid: false, path: sess.path }; + } } - return true; + return { isValid: true }; }, }), }); From 09125323c2a4d4d9702e68755072f38ad10a2c6c Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Wed, 27 Nov 2019 10:03:32 -0500 Subject: [PATCH 113/128] [Lens] Remove client-side reference to server source code (#51763) --- x-pack/legacy/plugins/lens/common/constants.ts | 2 +- x-pack/legacy/plugins/lens/index.ts | 4 +--- x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx | 3 ++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/lens/common/constants.ts b/x-pack/legacy/plugins/lens/common/constants.ts index c2eed1940fa1a..57f2a633e4524 100644 --- a/x-pack/legacy/plugins/lens/common/constants.ts +++ b/x-pack/legacy/plugins/lens/common/constants.ts @@ -5,7 +5,7 @@ */ export const PLUGIN_ID = 'lens'; - +export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; export const BASE_APP_URL = '/app/kibana'; export const BASE_API_URL = '/api/lens'; diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index a79b9907f6437..c4a684381b17c 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -9,11 +9,9 @@ import { resolve } from 'path'; import { LegacyPluginInitializer } from 'src/legacy/types'; import KbnServer, { Server } from 'src/legacy/server/kbn_server'; import mappings from './mappings.json'; -import { PLUGIN_ID, getEditPath } from './common'; +import { PLUGIN_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from './common'; import { lensServerPlugin } from './server'; -export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; - export const lens: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ id: PLUGIN_ID, diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 93f5928f58aa1..f2678463f57da 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -38,7 +38,7 @@ import { stopReportManager, trackUiEvent, } from '../lens_ui_telemetry'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../index'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../common'; import { KibanaLegacySetup } from '../../../../../../src/plugins/kibana_legacy/public'; import { EditorFrameStart } from '../types'; @@ -50,6 +50,7 @@ export interface LensPluginStartDependencies { data: DataPublicPluginStart; dataShim: DataStart; } + export class AppPlugin { private startDependencies: { data: DataPublicPluginStart; From dbca711524d23fc2e159b1f4db9ee63148ae80e2 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Wed, 27 Nov 2019 10:24:21 -0500 Subject: [PATCH 114/128] [SIEM][Detection Engine] Adds ecs threat properties to rules (#51782) * allows addition of ecs threat properties to rules and signals for mitre attack info * adds default empty array to threats on creation of rule, removes optional from update rules schema as it is implied, updates and adds relevant tests * adds sample rule with mitre attack threats property --- .../alerts/__mocks__/es_results.ts | 1 + .../detection_engine/alerts/create_rules.ts | 2 + .../alerts/rules_alert_type.ts | 1 + .../lib/detection_engine/alerts/types.ts | 14 + .../detection_engine/alerts/update_rules.ts | 2 + .../lib/detection_engine/alerts/utils.ts | 1 + .../routes/__mocks__/request_responses.ts | 22 ++ .../routes/create_rules_route.ts | 3 +- .../detection_engine/routes/schemas.test.ts | 364 +++++++++++++++++- .../lib/detection_engine/routes/schemas.ts | 27 ++ .../routes/update_rules_route.ts | 2 + .../lib/detection_engine/routes/utils.test.ts | 135 +++++++ .../lib/detection_engine/routes/utils.ts | 1 + .../scripts/rules/root_or_admin_threats.json | 43 +++ .../lib/detection_engine/signals_mapping.json | 3 + 15 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_threats.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 8080bd5ddd913..bed466dd9b94f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -35,6 +35,7 @@ export const sampleRuleAlertParams = ( filters: undefined, savedId: undefined, meta: undefined, + threats: undefined, }); export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts index 7c66714484383..4418bbc52b57d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts @@ -30,6 +30,7 @@ export const createRules = async ({ name, severity, tags, + threats, to, type, references, @@ -57,6 +58,7 @@ export const createRules = async ({ riskScore, severity, tags, + threats, to, type, references, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts index 91d7d18a4945c..61fe9c7c22639 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts @@ -48,6 +48,7 @@ export const rulesAlertType = ({ riskScore: schema.number(), severity: schema.string(), tags: schema.arrayOf(schema.string(), { defaultValue: [] }), + threats: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), to: schema.string(), type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 462a9b7d65ee2..c17e01f056f81 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -21,6 +21,19 @@ import { esFilters } from '../../../../../../../../src/plugins/data/server'; export type PartialFilter = Partial; +export interface ThreatParams { + framework: string; + tactic: { + id: string; + name: string; + reference: string; + }; + technique: { + id: string; + name: string; + reference: string; + }; +} export interface RuleAlertParams { description: string; enabled: boolean; @@ -44,6 +57,7 @@ export interface RuleAlertParams { severity: string; tags: string[]; to: string; + threats: ThreatParams[] | undefined | null; type: 'filter' | 'query' | 'saved_query'; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts index 81360d7824230..d6b828642433d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts @@ -65,6 +65,7 @@ export const updateRules = async ({ name, severity, tags, + threats, to, type, references, @@ -101,6 +102,7 @@ export const updateRules = async ({ riskScore, severity, tags, + threats, to, type, references, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index c3988b8fea458..9dedda6d79839 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -64,6 +64,7 @@ export const buildRule = ({ filters: ruleParams.filters, created_by: createdBy, updated_by: updatedBy, + threats: ruleParams.threats, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 4c49326fbb32a..cf76987aa38ad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -26,6 +26,13 @@ export const typicalPayload = (): Partial> = severity: 'high', query: 'user.name: root or user.name: admin', language: 'kuery', + threats: [ + { + framework: 'fake', + tactic: { id: 'fakeId', name: 'fakeName', reference: 'fakeRef' }, + technique: { id: 'techniqueId', name: 'techniqueName', reference: 'techniqueRef' }, + }, + ], }); export const typicalFilterPayload = (): Partial => ({ @@ -139,6 +146,21 @@ export const getResult = (): RuleAlertType => ({ tags: [], to: 'now', type: 'query', + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], references: ['http://www.example.com', 'https://ww.example.com'], }, interval: '5m', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts index 7e1ac07e1f0aa..4ff3a9b96b93e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -50,11 +50,11 @@ export const createCreateRulesRoute: Hapi.ServerRoute = { name, severity, tags, + threats, to, type, references, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; @@ -94,6 +94,7 @@ export const createCreateRulesRoute: Hapi.ServerRoute = { tags, to, type, + threats, references, }); return transformOrError(createdRule); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 6c7e5c4054326..3c85618452d8c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -5,7 +5,12 @@ */ import { createRulesSchema, updateRulesSchema, findRulesSchema, queryRulesSchema } from './schemas'; -import { RuleAlertParamsRest, FindParamsRest, UpdateRuleAlertParamsRest } from '../alerts/types'; +import { + RuleAlertParamsRest, + FindParamsRest, + UpdateRuleAlertParamsRest, + ThreatParams, +} from '../alerts/types'; describe('schemas', () => { describe('create rules schema', () => { @@ -240,6 +245,61 @@ describe('schemas', () => { }).error ).toBeFalsy(); }); + test('You can send in an empty array to threats', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [], + }).error + ).toBeFalsy(); + }); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + threats: [ + { + framework: 'someFramework', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error + ).toBeFalsy(); + }); test('If filter type is set then filter is required', () => { expect( @@ -736,6 +796,116 @@ describe('schemas', () => { ).toBeTruthy(); }); + test('You cannot send in an array of threats that are missing "framework"', () => { + expect( + createRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error + ).toBeTruthy(); + }); + test('You cannot send in an array of threats that are missing "tactic"', () => { + expect( + createRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + technique: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error + ).toBeTruthy(); + }); + test('You cannot send in an array of threats that are missing "techniques"', () => { + expect( + createRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }).error + ).toBeTruthy(); + }); + test('You can optionally send in an array of false positives', () => { expect( createRulesSchema.validate>({ @@ -1810,6 +1980,198 @@ describe('schemas', () => { }).error ).toBeTruthy(); }); + + test('threats is not defaulted to empty array on update', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).value.threats + ).toBe(undefined); + }); + }); + test('threats is not defaulted to undefined on update with empty array', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [], + }).value.threats + ).toMatchObject([]); + }); + test('threats is valid when updated with all sub-objects', () => { + const expected: ThreatParams[] = [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ]; + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).value.threats + ).toMatchObject(expected); + }); + test('threats is invalid when updated with missing property framework', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error + ).toBeTruthy(); + }); + test('threats is invalid when updated with missing tactic sub-object', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + technique: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error + ).toBeTruthy(); + }); + test('threats is invalid when updated with missing technique sub-object', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error + ).toBeTruthy(); }); describe('find rules schema', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index 664a98ad7d7dd..0b4f1094549a4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -50,6 +50,31 @@ const tags = Joi.array().items(Joi.string()); const fields = Joi.array() .items(Joi.string()) .single(); +const threat_framework = Joi.string(); +const threat_tactic_id = Joi.string(); +const threat_tactic_name = Joi.string(); +const threat_tactic_reference = Joi.string(); +const threat_tactic = Joi.object({ + id: threat_tactic_id.required(), + name: threat_tactic_name.required(), + reference: threat_tactic_reference.required(), +}); +const threat_technique_id = Joi.string(); +const threat_technique_name = Joi.string(); +const threat_technique_reference = Joi.string(); +const threat_technique = Joi.object({ + id: threat_technique_id.required(), + name: threat_technique_name.required(), + reference: threat_technique_reference.required(), +}); + +const threats = Joi.array().items( + Joi.object({ + framework: threat_framework.required(), + tactic: threat_tactic.required(), + technique: threat_technique.required(), + }) +); /* eslint-enable @typescript-eslint/camelcase */ export const createRulesSchema = Joi.object({ @@ -110,6 +135,7 @@ export const createRulesSchema = Joi.object({ tags: tags.default([]), to: to.required(), type: type.required(), + threats: threats.default([]), references: references.default([]), }); @@ -165,6 +191,7 @@ export const updateRulesSchema = Joi.object({ tags, to, type, + threats, references, }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts index 1cc65054527c0..c5fb1675fb343 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts @@ -50,6 +50,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { tags, to, type, + threats, references, } = request.payload; @@ -86,6 +87,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { tags, to, type, + threats, references, }); if (rule != null) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 632778d78dab7..4ef2d87d1d736 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -39,6 +39,21 @@ describe('utils', () => { severity: 'high', updated_by: 'elastic', tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], to: 'now', type: 'query', }); @@ -66,6 +81,21 @@ describe('utils', () => { severity: 'high', updated_by: 'elastic', tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], to: 'now', type: 'query', }); @@ -95,6 +125,21 @@ describe('utils', () => { severity: 'high', updated_by: 'elastic', tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], to: 'now', type: 'query', }); @@ -124,6 +169,21 @@ describe('utils', () => { severity: 'high', updated_by: 'elastic', tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], to: 'now', type: 'query', }); @@ -151,6 +211,21 @@ describe('utils', () => { severity: 'high', updated_by: 'elastic', tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], to: 'now', type: 'query', }); @@ -181,6 +256,21 @@ describe('utils', () => { severity: 'high', updated_by: 'elastic', tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], to: 'now', type: 'query', }); @@ -211,6 +301,21 @@ describe('utils', () => { severity: 'high', updated_by: 'elastic', tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], to: 'now', type: 'query', }); @@ -294,6 +399,21 @@ describe('utils', () => { tags: [], to: 'now', type: 'query', + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], }, ], }); @@ -331,6 +451,21 @@ describe('utils', () => { tags: [], to: 'now', type: 'query', + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + }, + ], }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index eb0ae49436bca..947fb27a89c3a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -54,6 +54,7 @@ export const transformAlertToRule = (alert: RuleAlertType): Partial Date: Wed, 27 Nov 2019 16:47:22 +0100 Subject: [PATCH 115/128] Split legacy plugin discovery, expose SavedObjects scopedClient, wrappers, repository (#48882) * Split legacy plugin discovery, expose internal SavedObjectsClient * Expose internal SavedObjectsClient to plugins * Add more documentation * Expose client wrappers, repository, scoped client from SavedObjects * Remove unused onBeforeWrite * Refactor Service / Repository for testability * Bind exposed clientProvider methods * Fix eArchiver's KibanaMigrator * Cleanup * Use APICaller type * Expose SavedObjectsServiceStart to plugins * API documentation * Rename API methods to be verbs --- .../server/kibana-plugin-server.coresetup.md | 1 + ...na-plugin-server.coresetup.savedobjects.md | 13 ++ .../server/kibana-plugin-server.corestart.md | 7 + ...na-plugin-server.corestart.savedobjects.md | 13 ++ ...a-plugin-server.isavedobjectsrepository.md | 13 ++ .../core/server/kibana-plugin-server.md | 7 + ...server.savedobjectsclient._constructor_.md | 20 -- ...kibana-plugin-server.savedobjectsclient.md | 11 +- ...plugin-server.savedobjectsclientfactory.md | 15 ++ ...er.savedobjectsdeletebynamespaceoptions.md | 19 ++ ...objectsdeletebynamespaceoptions.refresh.md | 13 ++ ...ver.savedobjectsincrementcounteroptions.md | 20 ++ ...ncrementcounteroptions.migrationversion.md | 11 + ...dobjectsincrementcounteroptions.refresh.md | 13 ++ ...erver.savedobjectsrepository.bulkcreate.md | 27 +++ ...n-server.savedobjectsrepository.bulkget.md | 31 +++ ...erver.savedobjectsrepository.bulkupdate.md | 27 +++ ...in-server.savedobjectsrepository.create.md | 28 +++ ...in-server.savedobjectsrepository.delete.md | 28 +++ ...avedobjectsrepository.deletebynamespace.md | 27 +++ ...ugin-server.savedobjectsrepository.find.md | 24 +++ ...lugin-server.savedobjectsrepository.get.md | 28 +++ ...savedobjectsrepository.incrementcounter.md | 43 ++++ ...na-plugin-server.savedobjectsrepository.md | 31 +++ ...in-server.savedobjectsrepository.update.md | 29 +++ ...vedobjectsservicesetup.addclientwrapper.md | 13 ++ ...tsservicesetup.createinternalrepository.md | 18 ++ ...ectsservicesetup.createscopedrepository.md | 18 ++ ...-plugin-server.savedobjectsservicesetup.md | 35 ++++ ...vedobjectsservicesetup.setclientfactory.md | 13 ++ ...avedobjectsservicestart.getscopedclient.md | 15 ++ ...-plugin-server.savedobjectsservicestart.md | 20 ++ src/core/server/index.ts | 15 +- src/core/server/internal_types.ts | 8 +- src/core/server/legacy/legacy_service.test.ts | 25 ++- src/core/server/legacy/legacy_service.ts | 31 ++- src/core/server/mocks.ts | 18 +- src/core/server/plugins/plugin.test.ts | 2 + src/core/server/plugins/plugin_context.ts | 10 +- .../server/plugins/plugins_service.test.ts | 1 + src/core/server/plugins/plugins_service.ts | 4 +- .../server/plugins/plugins_system.test.ts | 8 +- src/core/server/saved_objects/index.ts | 14 +- .../migrations/core/build_index_map.ts | 1 + .../migrations/kibana/kibana_migrator.test.ts | 5 +- .../migrations/kibana/kibana_migrator.ts | 6 +- .../saved_objects_service.mock.ts | 30 ++- .../saved_objects_service.test.ts | 21 +- .../saved_objects/saved_objects_service.ts | 189 +++++++++++++++--- .../server/saved_objects/schema/schema.ts | 1 + .../server/saved_objects/service/index.ts | 1 + .../service/lib/create_repository.test.ts | 119 +++++++++++ .../service/lib/create_repository.ts | 61 ++++++ .../server/saved_objects/service/lib/index.ts | 8 +- .../service/lib/repository.mock.ts | 35 ++++ .../service/lib/repository.test.js | 110 +--------- .../saved_objects/service/lib/repository.ts | 36 +++- .../service/lib/scoped_client_provider.ts | 5 +- .../service/saved_objects_client.ts | 16 +- src/core/server/server.api.md | 67 ++++++- src/core/server/server.test.mocks.ts | 6 +- src/core/server/server.ts | 28 ++- src/es_archiver/lib/indices/kibana_index.js | 3 +- 63 files changed, 1251 insertions(+), 264 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.coresetup.savedobjects.md create mode 100644 docs/development/core/server/kibana-plugin-server.corestart.savedobjects.md create mode 100644 docs/development/core/server/kibana-plugin-server.isavedobjectsrepository.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient._constructor_.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclientfactory.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsdeletebynamespaceoptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsdeletebynamespaceoptions.refresh.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.migrationversion.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.refresh.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.delete.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.deletebynamespace.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.incrementcounter.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.md create mode 100644 src/core/server/saved_objects/service/lib/create_repository.test.ts create mode 100644 src/core/server/saved_objects/service/lib/create_repository.ts create mode 100644 src/core/server/saved_objects/service/lib/repository.mock.ts diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index c51459bc41a43..1ad1641beb83b 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -19,5 +19,6 @@ export interface CoreSetup | [context](./kibana-plugin-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | +| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | | [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.savedobjects.md b/docs/development/core/server/kibana-plugin-server.coresetup.savedobjects.md new file mode 100644 index 0000000000000..96acc1ffce194 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.savedobjects.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) + +## CoreSetup.savedObjects property + +[SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) + +Signature: + +```typescript +savedObjects: SavedObjectsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-server.corestart.md b/docs/development/core/server/kibana-plugin-server.corestart.md index da80ae8be93af..a675c45a29820 100644 --- a/docs/development/core/server/kibana-plugin-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-server.corestart.md @@ -11,3 +11,10 @@ Context passed to the plugins `start` method. ```typescript export interface CoreStart ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [savedObjects](./kibana-plugin-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) | + diff --git a/docs/development/core/server/kibana-plugin-server.corestart.savedobjects.md b/docs/development/core/server/kibana-plugin-server.corestart.savedobjects.md new file mode 100644 index 0000000000000..531b04e9eed07 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.corestart.savedobjects.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreStart](./kibana-plugin-server.corestart.md) > [savedObjects](./kibana-plugin-server.corestart.savedobjects.md) + +## CoreStart.savedObjects property + +[SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) + +Signature: + +```typescript +savedObjects: SavedObjectsServiceStart; +``` diff --git a/docs/development/core/server/kibana-plugin-server.isavedobjectsrepository.md b/docs/development/core/server/kibana-plugin-server.isavedobjectsrepository.md new file mode 100644 index 0000000000000..7863d1b0ca49d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.isavedobjectsrepository.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ISavedObjectsRepository](./kibana-plugin-server.isavedobjectsrepository.md) + +## ISavedObjectsRepository type + +See [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) + +Signature: + +```typescript +export declare type ISavedObjectsRepository = Pick; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 13e0ea3645f26..38c7ad75d1db9 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -22,6 +22,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | +| [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md). | ## Enumerations @@ -96,6 +97,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientProviderOptions](./kibana-plugin-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | | [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | | +| [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-server.savedobjectsdeletebynamespaceoptions.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) | Options controlling the export operation. | | [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | @@ -109,10 +111,13 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsImportRetry](./kibana-plugin-server.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | | [SavedObjectsImportUnknownError](./kibana-plugin-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | | [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | +| [SavedObjectsIncrementCounterOptions](./kibana-plugin-server.savedobjectsincrementcounteroptions.md) | | | [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) | | | [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. | | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | +| [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for creating and registering Saved Object client wrappers. | +| [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceStart API provides a scoped Saved Objects client for interacting with Saved Objects. | | [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | | | [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) | Return type from a function to validate cookie contents. | @@ -149,6 +154,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IClusterClient](./kibana-plugin-server.iclusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | | [IContextProvider](./kibana-plugin-server.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. | +| [ISavedObjectsRepository](./kibana-plugin-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) | | [IScopedClusterClient](./kibana-plugin-server.iscopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md). | | [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. | | [KnownHeaders](./kibana-plugin-server.knownheaders.md) | Set of well-known HTTP headers. | @@ -175,6 +181,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | +| [SavedObjectsClientFactory](./kibana-plugin-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [UiSettingsType](./kibana-plugin-server.uisettingstype.md) | UI element type to represent the settings. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient._constructor_.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient._constructor_.md deleted file mode 100644 index 0bcca3ec57b54..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [(constructor)](./kibana-plugin-server.savedobjectsclient._constructor_.md) - -## SavedObjectsClient.(constructor) - -Constructs a new instance of the `SavedObjectsClient` class - -Signature: - -```typescript -constructor(repository: SavedObjectsRepository); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| repository | SavedObjectsRepository | | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md index cc00934a1e1fd..17d29bb912c83 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md @@ -4,19 +4,12 @@ ## SavedObjectsClient class - Signature: ```typescript export declare class SavedObjectsClient ``` -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(repository)](./kibana-plugin-server.savedobjectsclient._constructor_.md) | | Constructs a new instance of the SavedObjectsClient class | - ## Properties | Property | Modifiers | Type | Description | @@ -37,3 +30,7 @@ export declare class SavedObjectsClient | [get(type, id, options)](./kibana-plugin-server.savedobjectsclient.get.md) | | Retrieves a single object | | [update(type, id, attributes, options)](./kibana-plugin-server.savedobjectsclient.update.md) | | Updates an SavedObject | +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `SavedObjectsClient` class. + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientfactory.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientfactory.md new file mode 100644 index 0000000000000..9e30759720680 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientfactory.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClientFactory](./kibana-plugin-server.savedobjectsclientfactory.md) + +## SavedObjectsClientFactory type + +Describes the factory used to create instances of the Saved Objects Client. + +Signature: + +```typescript +export declare type SavedObjectsClientFactory = ({ request, }: { + request: Request; +}) => SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsdeletebynamespaceoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsdeletebynamespaceoptions.md new file mode 100644 index 0000000000000..df4ce1b4b8428 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsdeletebynamespaceoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-server.savedobjectsdeletebynamespaceoptions.md) + +## SavedObjectsDeleteByNamespaceOptions interface + + +Signature: + +```typescript +export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-server.savedobjectsdeletebynamespaceoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsdeletebynamespaceoptions.refresh.md b/docs/development/core/server/kibana-plugin-server.savedobjectsdeletebynamespaceoptions.refresh.md new file mode 100644 index 0000000000000..2332520ac388f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsdeletebynamespaceoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-server.savedobjectsdeletebynamespaceoptions.md) > [refresh](./kibana-plugin-server.savedobjectsdeletebynamespaceoptions.refresh.md) + +## SavedObjectsDeleteByNamespaceOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.md new file mode 100644 index 0000000000000..38ee40157888f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-server.savedobjectsincrementcounteroptions.md) + +## SavedObjectsIncrementCounterOptions interface + + +Signature: + +```typescript +export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [migrationVersion](./kibana-plugin-server.savedobjectsincrementcounteroptions.migrationversion.md) | SavedObjectsMigrationVersion | | +| [refresh](./kibana-plugin-server.savedobjectsincrementcounteroptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.migrationversion.md new file mode 100644 index 0000000000000..3b80dea4fecde --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.migrationversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-server.savedobjectsincrementcounteroptions.md) > [migrationVersion](./kibana-plugin-server.savedobjectsincrementcounteroptions.migrationversion.md) + +## SavedObjectsIncrementCounterOptions.migrationVersion property + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.refresh.md b/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.refresh.md new file mode 100644 index 0000000000000..acd8d6f0916f9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsincrementcounteroptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsIncrementCounterOptions](./kibana-plugin-server.savedobjectsincrementcounteroptions.md) > [refresh](./kibana-plugin-server.savedobjectsincrementcounteroptions.refresh.md) + +## SavedObjectsIncrementCounterOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md new file mode 100644 index 0000000000000..003bc6ac72466 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkcreate.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [bulkCreate](./kibana-plugin-server.savedobjectsrepository.bulkcreate.md) + +## SavedObjectsRepository.bulkCreate() method + +Creates multiple documents at once + +Signature: + +```typescript +bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | Array<SavedObjectsBulkCreateObject<T>> | | +| options | SavedObjectsCreateOptions | | + +Returns: + +`Promise>` + +{promise} - {saved\_objects: \[\[{ id, type, version, references, attributes, error: { message } }\]} + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md new file mode 100644 index 0000000000000..605984d5dea30 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkget.md @@ -0,0 +1,31 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [bulkGet](./kibana-plugin-server.savedobjectsrepository.bulkget.md) + +## SavedObjectsRepository.bulkGet() method + +Returns an array of objects by id + +Signature: + +```typescript +bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsBulkGetObject[] | | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise>` + +{promise} - { saved\_objects: \[{ id, type, version, attributes }\] } + +## Example + +bulkGet(\[ { id: 'one', type: 'config' }, { id: 'foo', type: 'index-pattern' } \]) + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md new file mode 100644 index 0000000000000..52a73c83b4c3a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.bulkupdate.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [bulkUpdate](./kibana-plugin-server.savedobjectsrepository.bulkupdate.md) + +## SavedObjectsRepository.bulkUpdate() method + +Updates multiple objects in bulk + +Signature: + +```typescript +bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | Array<SavedObjectsBulkUpdateObject<T>> | | +| options | SavedObjectsBulkUpdateOptions | | + +Returns: + +`Promise>` + +{promise} - {saved\_objects: \[\[{ id, type, version, references, attributes, error: { message } }\]} + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md new file mode 100644 index 0000000000000..3a731629156e2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.create.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [create](./kibana-plugin-server.savedobjectsrepository.create.md) + +## SavedObjectsRepository.create() method + +Persists an object + +Signature: + +```typescript +create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| attributes | T | | +| options | SavedObjectsCreateOptions | | + +Returns: + +`Promise>` + +{promise} - { id, type, version, attributes } + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.delete.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.delete.md new file mode 100644 index 0000000000000..52c36d2da162d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.delete.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [delete](./kibana-plugin-server.savedobjectsrepository.delete.md) + +## SavedObjectsRepository.delete() method + +Deletes an object + +Signature: + +```typescript +delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| options | SavedObjectsDeleteOptions | | + +Returns: + +`Promise<{}>` + +{promise} + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.deletebynamespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.deletebynamespace.md new file mode 100644 index 0000000000000..ab6eb30e664f1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.deletebynamespace.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [deleteByNamespace](./kibana-plugin-server.savedobjectsrepository.deletebynamespace.md) + +## SavedObjectsRepository.deleteByNamespace() method + +Deletes all objects from the provided namespace. + +Signature: + +```typescript +deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| namespace | string | | +| options | SavedObjectsDeleteByNamespaceOptions | | + +Returns: + +`Promise` + +{promise} - { took, timed\_out, total, deleted, batches, version\_conflicts, noops, retries, failures } + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md new file mode 100644 index 0000000000000..3c2855ed9a50c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.find.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [find](./kibana-plugin-server.savedobjectsrepository.find.md) + +## SavedObjectsRepository.find() method + +Signature: + +```typescript +find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, } | SavedObjectsFindOptions | | + +Returns: + +`Promise>` + +{promise} - { saved\_objects: \[{ id, type, version, attributes }\], total, per\_page, page } + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md new file mode 100644 index 0000000000000..dd1d81f225937 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.get.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [get](./kibana-plugin-server.savedobjectsrepository.get.md) + +## SavedObjectsRepository.get() method + +Gets a single object + +Signature: + +```typescript +get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise>` + +{promise} - { id, type, version, attributes } + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.incrementcounter.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.incrementcounter.md new file mode 100644 index 0000000000000..f20e9a73d99a1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.incrementcounter.md @@ -0,0 +1,43 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [incrementCounter](./kibana-plugin-server.savedobjectsrepository.incrementcounter.md) + +## SavedObjectsRepository.incrementCounter() method + +Increases a counter field by one. Creates the document if one doesn't exist for the given id. + +Signature: + +```typescript +incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{ + id: string; + type: string; + updated_at: string; + references: any; + version: string; + attributes: any; + }>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| counterFieldName | string | | +| options | SavedObjectsIncrementCounterOptions | | + +Returns: + +`Promise<{ + id: string; + type: string; + updated_at: string; + references: any; + version: string; + attributes: any; + }>` + +{promise} + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.md new file mode 100644 index 0000000000000..019363776590c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.md @@ -0,0 +1,31 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) + +## SavedObjectsRepository class + +Signature: + +```typescript +export declare class SavedObjectsRepository +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [bulkCreate(objects, options)](./kibana-plugin-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once | +| [bulkGet(objects, options)](./kibana-plugin-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | +| [bulkUpdate(objects, options)](./kibana-plugin-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | +| [create(type, attributes, options)](./kibana-plugin-server.savedobjectsrepository.create.md) | | Persists an object | +| [delete(type, id, options)](./kibana-plugin-server.savedobjectsrepository.delete.md) | | Deletes an object | +| [deleteByNamespace(namespace, options)](./kibana-plugin-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | +| [find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, })](./kibana-plugin-server.savedobjectsrepository.find.md) | | | +| [get(type, id, options)](./kibana-plugin-server.savedobjectsrepository.get.md) | | Gets a single object | +| [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | +| [update(type, id, attributes, options)](./kibana-plugin-server.savedobjectsrepository.update.md) | | Updates an object | + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `SavedObjectsRepository` class. + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md new file mode 100644 index 0000000000000..15890ab9211aa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrepository.update.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) > [update](./kibana-plugin-server.savedobjectsrepository.update.md) + +## SavedObjectsRepository.update() method + +Updates an object + +Signature: + +```typescript +update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| attributes | Partial<T> | | +| options | SavedObjectsUpdateOptions | | + +Returns: + +`Promise>` + +{promise} + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md new file mode 100644 index 0000000000000..e787d737ada17 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) > [addClientWrapper](./kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) + +## SavedObjectsServiceSetup.addClientWrapper property + +Add a client wrapper with the given priority. + +Signature: + +```typescript +addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md new file mode 100644 index 0000000000000..492aa1a2453a1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) > [createInternalRepository](./kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) + +## SavedObjectsServiceSetup.createInternalRepository property + +Creates a [Saved Objects repository](./kibana-plugin-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. + +Signature: + +```typescript +createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository; +``` + +## Remarks + +The repository should only be used for creating and registering a client factory or client wrapper. Using the repository directly for interacting with Saved Objects is an anti-pattern. Use the Saved Objects client from the [SavedObjectsServiceStart\#getScopedClient](./kibana-plugin-server.savedobjectsservicestart.md) method or the [route handler context](./kibana-plugin-server.requesthandlercontext.md) instead. + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md new file mode 100644 index 0000000000000..fc5aa40c21a20 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) > [createScopedRepository](./kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) + +## SavedObjectsServiceSetup.createScopedRepository property + +Creates a [Saved Objects repository](./kibana-plugin-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. + +Signature: + +```typescript +createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository; +``` + +## Remarks + +The repository should only be used for creating and registering a client factory or client wrapper. Using the repository directly for interacting with Saved Objects is an anti-pattern. Use the Saved Objects client from the [SavedObjectsServiceStart\#getScopedClient](./kibana-plugin-server.savedobjectsservicestart.md) method or the [route handler context](./kibana-plugin-server.requesthandlercontext.md) instead. + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md new file mode 100644 index 0000000000000..dd97b45f590e2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md @@ -0,0 +1,35 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) + +## SavedObjectsServiceSetup interface + +Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for creating and registering Saved Object client wrappers. + +Signature: + +```typescript +export interface SavedObjectsServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [addClientWrapper](./kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory<KibanaRequest>) => void | Add a client wrapper with the given priority. | +| [createInternalRepository](./kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) | (extraTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. | +| [createScopedRepository](./kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. | +| [setClientFactory](./kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | (customClientFactory: SavedObjectsClientFactory<KibanaRequest>) => void | Set a default factory for creating Saved Objects clients. Only one client factory can be set, subsequent calls to this method will fail. | + +## Remarks + +Note: The Saved Object setup API's should only be used for creating and registering client wrappers. Constructing a Saved Objects client or repository for use within your own plugin won't have any of the registered wrappers applied and is considered an anti-pattern. Use the Saved Objects client from the [SavedObjectsServiceStart\#getScopedClient](./kibana-plugin-server.savedobjectsservicestart.md) method or the [route handler context](./kibana-plugin-server.requesthandlercontext.md) instead. + +When plugins access the Saved Objects client, a new client is created using the factory provided to `setClientFactory` and wrapped by all wrappers registered through `addClientWrapper`. To create a factory or wrapper, plugins will have to construct a Saved Objects client. First create a repository by calling `scopedRepository` or `internalRepository` and then use this repository as the argument to the [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) constructor. + +## Example + +import {SavedObjectsClient, CoreSetup} from 'src/core/server'; + +export class Plugin() { setup: (core: CoreSetup) => { core.savedObjects.setClientFactory(({request: KibanaRequest}) => { return new SavedObjectsClient(core.savedObjects.scopedRepository(request)); }) } } + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md new file mode 100644 index 0000000000000..544e0b9d5fa73 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) > [setClientFactory](./kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) + +## SavedObjectsServiceSetup.setClientFactory property + +Set a default factory for creating Saved Objects clients. Only one client factory can be set, subsequent calls to this method will fail. + +Signature: + +```typescript +setClientFactory: (customClientFactory: SavedObjectsClientFactory) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md new file mode 100644 index 0000000000000..e87979a124bdc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) > [getScopedClient](./kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) + +## SavedObjectsServiceStart.getScopedClient property + +Creates a [Saved Objects client](./kibana-plugin-server.savedobjectsclientcontract.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. If other plugins have registered Saved Objects client wrappers, these will be applied to extend the functionality of the client. + +A client that is already scoped to the incoming request is also exposed from the route handler context see [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md). + +Signature: + +```typescript +getScopedClient: (req: KibanaRequest, options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.md new file mode 100644 index 0000000000000..5a869b3b6c1cb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) + +## SavedObjectsServiceStart interface + +Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceStart API provides a scoped Saved Objects client for interacting with Saved Objects. + +Signature: + +```typescript +export interface SavedObjectsServiceStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getScopedClient](./kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) | (req: KibanaRequest, options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract | Creates a [Saved Objects client](./kibana-plugin-server.savedobjectsclientcontract.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. If other plugins have registered Saved Objects client wrappers, these will be applied to extend the functionality of the client.A client that is already scoped to the incoming request is also exposed from the route handler context see [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md). | + diff --git a/src/core/server/index.ts b/src/core/server/index.ts index b53f04d601ff4..f792f6e604c15 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -45,6 +45,7 @@ import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plug import { ContextSetup } from './context'; import { IUiSettingsClient, UiSettingsServiceSetup } from './ui_settings'; import { SavedObjectsClientContract } from './saved_objects/types'; +import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from './saved_objects'; export { bootstrap } from './bootstrap'; export { ConfigPath, ConfigService, EnvironmentMode, PackageInfo } from './config'; @@ -144,6 +145,7 @@ export { SavedObjectsClientProviderOptions, SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, + SavedObjectsClientFactory, SavedObjectsCreateOptions, SavedObjectsErrorHelpers, SavedObjectsExportOptions, @@ -165,7 +167,13 @@ export { SavedObjectsLegacyService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, + SavedObjectsServiceStart, + SavedObjectsServiceSetup, SavedObjectsDeleteOptions, + ISavedObjectsRepository, + SavedObjectsRepository, + SavedObjectsDeleteByNamespaceOptions, + SavedObjectsIncrementCounterOptions, } from './saved_objects'; export { @@ -233,6 +241,8 @@ export interface CoreSetup { elasticsearch: ElasticsearchServiceSetup; /** {@link HttpServiceSetup} */ http: HttpServiceSetup; + /** {@link SavedObjectsServiceSetup} */ + savedObjects: SavedObjectsServiceSetup; /** {@link UiSettingsServiceSetup} */ uiSettings: UiSettingsServiceSetup; } @@ -242,6 +252,9 @@ export interface CoreSetup { * * @public */ -export interface CoreStart {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface CoreStart { + /** {@link SavedObjectsServiceStart} */ + savedObjects: SavedObjectsServiceStart; +} export { ContextSetup, PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId }; diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 1330c5aee64fd..d1a65c6f3437e 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -21,7 +21,10 @@ import { InternalElasticsearchServiceSetup } from './elasticsearch'; import { InternalHttpServiceSetup } from './http'; import { InternalUiSettingsServiceSetup } from './ui_settings'; import { ContextSetup } from './context'; -import { SavedObjectsServiceStart } from './saved_objects'; +import { + InternalSavedObjectsServiceStart, + InternalSavedObjectsServiceSetup, +} from './saved_objects'; /** @internal */ export interface InternalCoreSetup { @@ -29,11 +32,12 @@ export interface InternalCoreSetup { http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; uiSettings: InternalUiSettingsServiceSetup; + savedObjects: InternalSavedObjectsServiceSetup; } /** * @internal */ export interface InternalCoreStart { - savedObjects: SavedObjectsServiceStart; + savedObjects: InternalSavedObjectsServiceStart; } diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 030caa8324521..286e1a0612c94 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -43,10 +43,9 @@ import { BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { DiscoveredPlugin } from '../plugins'; -import { KibanaMigrator } from '../saved_objects/migrations'; -import { ISavedObjectsClientProvider } from '../saved_objects'; import { httpServiceMock } from '../http/http_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; +import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; const MockKbnServer: jest.Mock = KbnServer as any; @@ -79,7 +78,7 @@ beforeEach(() => { getAuthHeaders: () => undefined, } as any, }, - + savedObjects: savedObjectsServiceMock.createSetupContract(), plugins: { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { @@ -94,10 +93,7 @@ beforeEach(() => { startDeps = { core: { - savedObjects: { - migrator: {} as KibanaMigrator, - clientProvider: {} as ISavedObjectsClientProvider, - }, + savedObjects: savedObjectsServiceMock.createStartContract(), plugins: { contracts: new Map() }, }, plugins: {}, @@ -128,6 +124,7 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); + await legacyService.discoverPlugins(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -153,6 +150,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); + await legacyService.discoverPlugins(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -180,6 +178,7 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); + await legacyService.discoverPlugins(); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"something failed"` @@ -199,9 +198,12 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(legacyService.discoverPlugins()).rejects.toThrowErrorMatchingInlineSnapshot( `"something failed"` ); + await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()"` + ); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Legacy service is not setup yet."` ); @@ -217,6 +219,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); + await legacyService.discoverPlugins(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -237,6 +240,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); + await legacyService.discoverPlugins(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -261,6 +265,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); + await legacyService.discoverPlugins(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -280,7 +285,7 @@ describe('once LegacyService is set up without connection info', () => { let legacyService: LegacyService; beforeEach(async () => { legacyService = new LegacyService({ coreId, env, logger, configService: configService as any }); - + await legacyService.discoverPlugins(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); }); @@ -329,6 +334,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { configService: configService as any, }); + await devClusterLegacyService.discoverPlugins(); await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); @@ -350,6 +356,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { configService: configService as any, }); + await devClusterLegacyService.discoverPlugins(); await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 99963ad9ce3e8..fd081b23a0ef2 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -74,14 +74,17 @@ export interface LegacyServiceStartDeps { } /** @internal */ -export interface LegacyServiceSetup { +export interface LegacyServiceDiscoverPlugins { pluginSpecs: LegacyPluginSpec[]; uiExports: SavedObjectsLegacyUiExports; pluginExtendedConfig: Config; } /** @internal */ -export class LegacyService implements CoreService { +export type ILegacyService = Pick; + +/** @internal */ +export class LegacyService implements CoreService { /** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */ public readonly legacyId = Symbol(); private readonly log: Logger; @@ -111,9 +114,7 @@ export class LegacyService implements CoreService { .pipe(map(rawConfig => new HttpConfig(rawConfig, coreContext.env))); } - public async setup(setupDeps: LegacyServiceSetupDeps) { - this.setupDeps = setupDeps; - + public async discoverPlugins(): Promise { this.update$ = this.coreContext.configService.getConfig$().pipe( tap(config => { if (this.kbnServer !== undefined) { @@ -164,6 +165,16 @@ export class LegacyService implements CoreService { }; } + public async setup(setupDeps: LegacyServiceSetupDeps) { + this.log.debug('setting up legacy service'); + if (!this.legacyRawConfig || !this.legacyPlugins || !this.settings) { + throw new Error( + 'Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()' + ); + } + this.setupDeps = setupDeps; + } + public async start(startDeps: LegacyServiceStartDeps) { const { setupDeps } = this; if (!setupDeps || !this.legacyRawConfig || !this.legacyPlugins || !this.settings) { @@ -249,11 +260,19 @@ export class LegacyService implements CoreService { basePath: setupDeps.core.http.basePath, isTlsEnabled: setupDeps.core.http.isTlsEnabled, }, + savedObjects: { + setClientFactory: setupDeps.core.savedObjects.setClientFactory, + addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, + createInternalRepository: setupDeps.core.savedObjects.createInternalRepository, + createScopedRepository: setupDeps.core.savedObjects.createScopedRepository, + }, uiSettings: { register: setupDeps.core.uiSettings.register, }, }; - const coreStart: CoreStart = {}; + const coreStart: CoreStart = { + savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient }, + }; // eslint-disable-next-line @typescript-eslint/no-var-requires const KbnServer = require('../../../legacy/server/kbn_server'); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index b51d5302e3274..a811efdf4b1b9 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -22,7 +22,9 @@ import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from './http/http_service.mock'; import { contextServiceMock } from './context/context_service.mock'; +import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; +import { InternalCoreSetup, InternalCoreStart } from './internal_types'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -87,6 +89,7 @@ function createCoreSetupMock() { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpMock, + savedObjects: savedObjectsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, }; @@ -94,24 +97,35 @@ function createCoreSetupMock() { } function createCoreStartMock() { - const mock: MockedKeys = {}; + const mock: MockedKeys = { + savedObjects: savedObjectsServiceMock.createStartContract(), + }; return mock; } function createInternalCoreSetupMock() { - const setupDeps = { + const setupDeps: InternalCoreSetup = { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), + savedObjects: savedObjectsServiceMock.createSetupContract(), }; return setupDeps; } +function createInternalCoreStartMock() { + const startDeps: InternalCoreStart = { + savedObjects: savedObjectsServiceMock.createStartContract(), + }; + return startDeps; +} + export const coreMock = { createSetup: createCoreSetupMock, createStart: createCoreStartMock, createInternalSetup: createInternalCoreSetupMock, + createInternalStart: createInternalCoreStartMock, createPluginInitializerContext: pluginInitializerContextMock, }; diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 6aab03a01675d..10259b718577c 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -66,7 +66,9 @@ configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); let coreId: symbol; let env: Env; let coreContext: CoreContext; + const setupDeps = coreMock.createInternalSetup(); + beforeEach(() => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 9885a572ad8c0..6edce1b2533cb 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -123,6 +123,12 @@ export function createPluginSetupContext( basePath: deps.http.basePath, isTlsEnabled: deps.http.isTlsEnabled, }, + savedObjects: { + setClientFactory: deps.savedObjects.setClientFactory, + addClientWrapper: deps.savedObjects.addClientWrapper, + createInternalRepository: deps.savedObjects.createInternalRepository, + createScopedRepository: deps.savedObjects.createScopedRepository, + }, uiSettings: { register: deps.uiSettings.register, }, @@ -146,5 +152,7 @@ export function createPluginStartContext( deps: PluginsServiceStartDeps, plugin: PluginWrapper ): CoreStart { - return {}; + return { + savedObjects: { getScopedClient: deps.savedObjects.getScopedClient }, + }; } diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 7e55faa43360e..df5473bc97d99 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -43,6 +43,7 @@ let configService: ConfigService; let coreId: symbol; let env: Env; let mockPluginSystem: jest.Mocked; + const setupDeps = coreMock.createInternalSetup(); const logger = loggingServiceMock.create(); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 4c73c2a304dc4..3f9999aad4ab9 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -28,7 +28,7 @@ import { PluginWrapper } from './plugin'; import { DiscoveredPlugin, PluginConfigDescriptor, PluginName, InternalPluginInfo } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; -import { InternalCoreSetup } from '../internal_types'; +import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { IConfigService } from '../config'; import { pick } from '../../utils'; @@ -63,7 +63,7 @@ export interface PluginsServiceStart { export type PluginsServiceSetupDeps = InternalCoreSetup; /** @internal */ -export interface PluginsServiceStartDeps {} // eslint-disable-line @typescript-eslint/no-empty-interface +export type PluginsServiceStartDeps = InternalCoreStart; /** @internal */ export class PluginsService implements CoreService { diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 6f1788f717f61..18c04af3bb641 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -33,7 +33,6 @@ import { loggingServiceMock } from '../logging/logging_service.mock'; import { PluginWrapper } from './plugin'; import { PluginName } from './types'; import { PluginsSystem } from './plugins_system'; - import { coreMock } from '../mocks'; const logger = loggingServiceMock.create(); @@ -68,7 +67,9 @@ const configService = configServiceMock.create(); configService.atPath.mockReturnValue(new BehaviorSubject({ initialize: true })); let env: Env; let coreContext: CoreContext; + const setupDeps = coreMock.createInternalSetup(); +const startDeps = coreMock.createInternalStart(); beforeEach(() => { env = Env.createDefault(getEnvOptions()); @@ -249,7 +250,6 @@ test('correctly orders plugins and returns exposed values for "setup" and "start expect(plugin.setup).toHaveBeenCalledWith(setupContextMap.get(plugin.name), deps.setup); } - const startDeps = {}; expect([...(await pluginsSystem.startPlugins(startDeps))]).toMatchInlineSnapshot(` Array [ Array [ @@ -382,7 +382,7 @@ test('`uiPlugins` returns only ui plugin dependencies', async () => { test('can start without plugins', async () => { await pluginsSystem.setupPlugins(setupDeps); - const pluginsStart = await pluginsSystem.startPlugins({}); + const pluginsStart = await pluginsSystem.startPlugins(startDeps); expect(pluginsStart).toBeInstanceOf(Map); expect(pluginsStart.size).toBe(0); @@ -400,7 +400,7 @@ test('`startPlugins` only starts plugins that were setup', async () => { pluginsSystem.addPlugin(plugin); }); await pluginsSystem.setupPlugins(setupDeps); - const result = await pluginsSystem.startPlugins({}); + const result = await pluginsSystem.startPlugins(startDeps); expect([...result]).toMatchInlineSnapshot(` Array [ Array [ diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 76c62e0841bff..1100c18bcc72f 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -35,6 +35,18 @@ export { SavedObjectsSerializer, RawDoc as SavedObjectsRawDoc } from './serializ export { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; -export { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects_service'; +export { + SavedObjectsService, + InternalSavedObjectsServiceStart, + SavedObjectsServiceStart, + SavedObjectsServiceSetup, + InternalSavedObjectsServiceSetup, +} from './saved_objects_service'; + +export { + ISavedObjectsRepository, + SavedObjectsIncrementCounterOptions, + SavedObjectsDeleteByNamespaceOptions, +} from './service/lib/repository'; export { config } from './saved_objects_config'; diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.ts b/src/core/server/saved_objects/migrations/core/build_index_map.ts index d7a26b7728f44..3b7ed20d94646 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.ts @@ -39,6 +39,7 @@ export interface IndexMap { * This file contains logic to convert savedObjectSchemas into a dictonary of indexes and documents */ export function createIndexMap({ + /** @deprecated Remove once savedObjectsSchemas are exposed from Core */ config, kibanaIndexName, schema, diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 51551ae4887b5..b89abc596ad18 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -20,6 +20,7 @@ import _ from 'lodash'; import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; import { loggingServiceMock } from '../../../logging/logging_service.mock'; +import { SavedObjectsSchema } from '../../schema'; describe('KibanaMigrator', () => { describe('getActiveMappings', () => { @@ -112,12 +113,12 @@ function mockOptions({ configValues }: { configValues?: any } = {}): KibanaMigra }, }, ], - savedObjectSchemas: { + savedObjectSchemas: new SavedObjectsSchema({ testtype2: { isNamespaceAgnostic: false, indexPattern: 'other-index', }, - }, + }), kibanaConfig: { enabled: true, index: '.my-index', diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 5bde5deec9382..1b01680c427ee 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -25,7 +25,7 @@ import { Logger } from 'src/core/server/logging'; import { KibanaConfigType } from 'src/core/server/kibana_config'; import { MappingProperties, SavedObjectsMapping, IndexMapping } from '../../mappings'; -import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from '../../schema'; +import { SavedObjectsSchema } from '../../schema'; import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; import { docValidator, PropertyValidators } from '../../validation'; import { buildActiveMappings, CallCluster, IndexMigrator } from '../core'; @@ -47,7 +47,7 @@ export interface KibanaMigratorOptions { logger: Logger; savedObjectMappings: SavedObjectsMapping[]; savedObjectMigrations: MigrationDefinition; - savedObjectSchemas: SavedObjectsSchemaDefinition; + savedObjectSchemas: SavedObjectsSchema; savedObjectValidations: PropertyValidators; } @@ -87,7 +87,7 @@ export class KibanaMigrator { this.callCluster = callCluster; this.kibanaConfig = kibanaConfig; this.savedObjectsConfig = savedObjectsConfig; - this.schema = new SavedObjectsSchema(savedObjectSchemas); + this.schema = savedObjectSchemas; this.serializer = new SavedObjectsSerializer(this.schema); this.mappingProperties = mergeProperties(savedObjectMappings || []); this.log = logger; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 0a021ee97e26a..b2596146a02d4 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -17,21 +17,44 @@ * under the License. */ -import { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects_service'; +import { + SavedObjectsService, + InternalSavedObjectsServiceSetup, + InternalSavedObjectsServiceStart, +} from './saved_objects_service'; import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; import { savedObjectsClientProviderMock } from './service/lib/scoped_client_provider.mock'; +import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; +import { savedObjectsClientMock } from './service/saved_objects_client.mock'; type SavedObjectsServiceContract = PublicMethodsOf; const createStartContractMock = () => { - const startContract: jest.Mocked = { + const startContract: jest.Mocked = { clientProvider: savedObjectsClientProviderMock.create(), + getScopedClient: jest.fn(), migrator: mockKibanaMigrator.create(), }; return startContract; }; +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + getScopedClient: jest.fn(), + setClientFactory: jest.fn(), + addClientWrapper: jest.fn(), + createInternalRepository: jest.fn(), + createScopedRepository: jest.fn(), + }; + + setupContract.getScopedClient.mockReturnValue(savedObjectsClientMock.create()); + setupContract.createInternalRepository.mockReturnValue(savedObjectsRepositoryMock.create()); + setupContract.createScopedRepository.mockReturnValue(savedObjectsRepositoryMock.create()); + + return setupContract; +}; + const createsavedObjectsServiceMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), @@ -39,7 +62,7 @@ const createsavedObjectsServiceMock = () => { stop: jest.fn(), }; - mocked.setup.mockResolvedValue({ clientProvider: savedObjectsClientProviderMock.create() }); + mocked.setup.mockResolvedValue(createSetupContractMock()); mocked.start.mockResolvedValue(createStartContractMock()); mocked.stop.mockResolvedValue(); return mocked; @@ -47,5 +70,6 @@ const createsavedObjectsServiceMock = () => { export const savedObjectsServiceMock = { create: createsavedObjectsServiceMock, + createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, }; diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index c31ad90011865..f58939c58e85e 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -27,7 +27,6 @@ import { of } from 'rxjs'; import * as legacyElasticsearch from 'elasticsearch'; import { Env } from '../config'; import { configServiceMock } from '../mocks'; -import { SavedObjectsClientProvider } from '.'; afterEach(() => { jest.clearAllMocks(); @@ -51,7 +50,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = ({ elasticsearch: { adminClient$: of(clusterClient) }, - legacy: { uiExports: { savedObjectMappings: [] }, pluginExtendedConfig: {} }, + legacyPlugins: { uiExports: { savedObjectMappings: [] }, pluginExtendedConfig: {} }, } as unknown) as SavedObjectsSetupDeps; await soService.setup(coreSetup, 1); @@ -60,18 +59,6 @@ describe('SavedObjectsService', () => { 'success' ); }); - - it('resolves with clientProvider', async () => { - const coreContext = mockCoreContext.create(); - const soService = new SavedObjectsService(coreContext); - const coreSetup = ({ - elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, - legacy: { uiExports: {}, pluginExtendedConfig: {} }, - } as unknown) as SavedObjectsSetupDeps; - - const savedObjectsSetup = await soService.setup(coreSetup); - expect(savedObjectsSetup.clientProvider).toBeInstanceOf(SavedObjectsClientProvider); - }); }); describe('#start()', () => { @@ -82,7 +69,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = ({ elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, - legacy: { uiExports: {}, pluginExtendedConfig: {} }, + legacyPlugins: { uiExports: {}, pluginExtendedConfig: {} }, } as unknown) as SavedObjectsSetupDeps; await soService.setup(coreSetup); @@ -96,7 +83,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = ({ elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, - legacy: { uiExports: {}, pluginExtendedConfig: {} }, + legacyPlugins: { uiExports: {}, pluginExtendedConfig: {} }, } as unknown) as SavedObjectsSetupDeps; await soService.setup(coreSetup); @@ -110,7 +97,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = ({ elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, - legacy: { uiExports: {}, pluginExtendedConfig: {} }, + legacyPlugins: { uiExports: {}, pluginExtendedConfig: {} }, } as unknown) as SavedObjectsSetupDeps; await soService.setup(coreSetup); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 43c3afa3ed639..589cd9cce400f 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -22,40 +22,159 @@ import { first } from 'rxjs/operators'; import { SavedObjectsClient, SavedObjectsSchema, - SavedObjectsRepository, - SavedObjectsSerializer, SavedObjectsClientProvider, ISavedObjectsClientProvider, + SavedObjectsClientProviderOptions, } from './'; -import { getRootPropertiesObjects } from './mappings'; import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; -import { LegacyServiceSetup } from '../legacy/legacy_service'; -import { ElasticsearchServiceSetup } from '../elasticsearch'; +import { LegacyServiceDiscoverPlugins } from '../legacy/legacy_service'; +import { ElasticsearchServiceSetup, APICaller } from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; -import { retryCallCluster, migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster'; +import { migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster'; import { SavedObjectsConfigType } from './saved_objects_config'; import { KibanaRequest } from '../http'; +import { SavedObjectsClientContract } from './types'; +import { ISavedObjectsRepository } from './service/lib/repository'; +import { + SavedObjectsClientFactory, + SavedObjectsClientWrapperFactory, +} from './service/lib/scoped_client_provider'; +import { createRepository } from './service/lib/create_repository'; import { Logger } from '..'; /** + * Saved Objects is Kibana's data persisentence mechanism allowing plugins to + * use Elasticsearch for storing and querying state. The + * SavedObjectsServiceSetup API exposes methods for creating and registering + * Saved Object client wrappers. + * + * @remarks + * Note: The Saved Object setup API's should only be used for creating and + * registering client wrappers. Constructing a Saved Objects client or + * repository for use within your own plugin won't have any of the registered + * wrappers applied and is considered an anti-pattern. Use the Saved Objects + * client from the + * {@link SavedObjectsServiceStart | SavedObjectsServiceStart#getScopedClient } + * method or the {@link RequestHandlerContext | route handler context} instead. + * + * When plugins access the Saved Objects client, a new client is created using + * the factory provided to `setClientFactory` and wrapped by all wrappers + * registered through `addClientWrapper`. To create a factory or wrapper, + * plugins will have to construct a Saved Objects client. First create a + * repository by calling `scopedRepository` or `internalRepository` and then + * use this repository as the argument to the {@link SavedObjectsClient} + * constructor. + * + * @example + * import {SavedObjectsClient, CoreSetup} from 'src/core/server'; + * + * export class Plugin() { + * setup: (core: CoreSetup) => { + * core.savedObjects.setClientFactory(({request: KibanaRequest}) => { + * return new SavedObjectsClient(core.savedObjects.scopedRepository(request)); + * }) + * } + * } + * * @public */ export interface SavedObjectsServiceSetup { - clientProvider: ISavedObjectsClientProvider; + /** + * Set a default factory for creating Saved Objects clients. Only one client + * factory can be set, subsequent calls to this method will fail. + */ + setClientFactory: (customClientFactory: SavedObjectsClientFactory) => void; + + /** + * Add a client wrapper with the given priority. + */ + addClientWrapper: ( + priority: number, + id: string, + factory: SavedObjectsClientWrapperFactory + ) => void; + + /** + * Creates a {@link ISavedObjectsRepository | Saved Objects repository} that + * uses the credentials from the passed in request to authenticate with + * Elasticsearch. + * + * @remarks + * The repository should only be used for creating and registering a client + * factory or client wrapper. Using the repository directly for interacting + * with Saved Objects is an anti-pattern. Use the Saved Objects client from + * the + * {@link SavedObjectsServiceStart | SavedObjectsServiceStart#getScopedClient } + * method or the {@link RequestHandlerContext | route handler context} + * instead. + */ + createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository; + + /** + * Creates a {@link ISavedObjectsRepository | Saved Objects repository} that + * uses the internal Kibana user for authenticating with Elasticsearch. + * + * @remarks + * The repository should only be used for creating and registering a client + * factory or client wrapper. Using the repository directly for interacting + * with Saved Objects is an anti-pattern. Use the Saved Objects client from + * the + * {@link SavedObjectsServiceStart | SavedObjectsServiceStart#getScopedClient } + * method or the {@link RequestHandlerContext | route handler context} + * instead. + */ + createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository; +} + +/** + * @internal + */ +export interface InternalSavedObjectsServiceSetup extends SavedObjectsServiceSetup { + getScopedClient: ( + req: KibanaRequest, + options?: SavedObjectsClientProviderOptions + ) => SavedObjectsClientContract; } /** + * Saved Objects is Kibana's data persisentence mechanism allowing plugins to + * use Elasticsearch for storing and querying state. The + * SavedObjectsServiceStart API provides a scoped Saved Objects client for + * interacting with Saved Objects. + * * @public */ export interface SavedObjectsServiceStart { + /** + * Creates a {@link SavedObjectsClientContract | Saved Objects client} that + * uses the credentials from the passed in request to authenticate with + * Elasticsearch. If other plugins have registered Saved Objects client + * wrappers, these will be applied to extend the functionality of the client. + * + * A client that is already scoped to the incoming request is also exposed + * from the route handler context see {@link RequestHandlerContext}. + */ + getScopedClient: ( + req: KibanaRequest, + options?: SavedObjectsClientProviderOptions + ) => SavedObjectsClientContract; +} + +export interface InternalSavedObjectsServiceStart extends SavedObjectsServiceStart { + /** + * @deprecated Exposed only for injecting into Legacy + */ migrator: IKibanaMigrator; + /** + * @deprecated Exposed only for injecting into Legacy + */ clientProvider: ISavedObjectsClientProvider; } /** @internal */ export interface SavedObjectsSetupDeps { - legacy: LegacyServiceSetup; + legacyPlugins: LegacyServiceDiscoverPlugins; elasticsearch: ElasticsearchServiceSetup; } @@ -64,7 +183,7 @@ export interface SavedObjectsSetupDeps { export interface SavedObjectsStartDeps {} export class SavedObjectsService - implements CoreService { + implements CoreService { private migrator: KibanaMigrator | undefined; private logger: Logger; private clientProvider: ISavedObjectsClientProvider | undefined; @@ -74,19 +193,21 @@ export class SavedObjectsService } public async setup( - coreSetup: SavedObjectsSetupDeps, + setupDeps: SavedObjectsSetupDeps, migrationsRetryDelay?: number - ): Promise { + ): Promise { this.logger.debug('Setting up SavedObjects service'); const { - savedObjectSchemas, + savedObjectSchemas: savedObjectsSchemasDefinition, savedObjectMappings, savedObjectMigrations, savedObjectValidations, - } = await coreSetup.legacy.uiExports; + } = setupDeps.legacyPlugins.uiExports; + + const savedObjectSchemas = new SavedObjectsSchema(savedObjectsSchemasDefinition); - const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(first()).toPromise(); + const adminClient = await setupDeps.elasticsearch.adminClient$.pipe(first()).toPromise(); const kibanaConfig = await this.coreContext.configService .atPath('kibana') @@ -105,7 +226,7 @@ export class SavedObjectsService savedObjectValidations, logger: this.coreContext.logger.get('migrations'), kibanaVersion: this.coreContext.env.packageInfo.version, - config: coreSetup.legacy.pluginExtendedConfig, + config: setupDeps.legacyPlugins.pluginExtendedConfig, savedObjectsConfig, kibanaConfig, callCluster: migrationsRetryCallCluster( @@ -115,35 +236,36 @@ export class SavedObjectsService ), })); - const mappings = this.migrator.getActiveMappings(); - const allTypes = Object.keys(getRootPropertiesObjects(mappings)); - const schema = new SavedObjectsSchema(savedObjectSchemas); - const serializer = new SavedObjectsSerializer(schema); - const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type)); + const createSORepository = (callCluster: APICaller, extraTypes: string[] = []) => { + return createRepository( + migrator, + savedObjectSchemas, + setupDeps.legacyPlugins.pluginExtendedConfig, + kibanaConfig.index, + callCluster, + extraTypes + ); + }; this.clientProvider = new SavedObjectsClientProvider({ defaultClientFactory({ request }) { - const repository = new SavedObjectsRepository({ - index: kibanaConfig.index, - config: coreSetup.legacy.pluginExtendedConfig, - migrator, - mappings, - schema, - serializer, - allowedTypes: visibleTypes, - callCluster: retryCallCluster(adminClient.asScoped(request).callAsCurrentUser), - }); - + const repository = createSORepository(adminClient.asScoped(request).callAsCurrentUser); return new SavedObjectsClient(repository); }, }); return { - clientProvider: this.clientProvider, + getScopedClient: this.clientProvider.getClient.bind(this.clientProvider), + setClientFactory: this.clientProvider.setClientFactory.bind(this.clientProvider), + addClientWrapper: this.clientProvider.addClientWrapperFactory.bind(this.clientProvider), + createInternalRepository: (extraTypes?: string[]) => + createSORepository(adminClient.callAsInternalUser, extraTypes), + createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => + createSORepository(adminClient.asScoped(req).callAsCurrentUser, extraTypes), }; } - public async start(core: SavedObjectsStartDeps): Promise { + public async start(core: SavedObjectsStartDeps): Promise { if (!this.clientProvider) { throw new Error('#setup() needs to be run first'); } @@ -171,6 +293,7 @@ export class SavedObjectsService return { migrator: this.migrator!, clientProvider: this.clientProvider, + getScopedClient: this.clientProvider.getClient.bind(this.clientProvider), }; } diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts index 06d29bf7dcf32..6be5ca9bfce60 100644 --- a/src/core/server/saved_objects/schema/schema.ts +++ b/src/core/server/saved_objects/schema/schema.ts @@ -46,6 +46,7 @@ export class SavedObjectsSchema { return false; } + // TODO: Remove dependency on config when we move SavedObjectsSchema to NP public getIndexForType(config: Config, type: string): string | undefined { if (this.definition != null && this.definition.hasOwnProperty(type)) { const { indexPattern } = this.definition[type]; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index cf0769fced460..f50ee1759dad7 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -58,6 +58,7 @@ export { SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, SavedObjectsErrorHelpers, + SavedObjectsClientFactory, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/create_repository.test.ts b/src/core/server/saved_objects/service/lib/create_repository.test.ts new file mode 100644 index 0000000000000..d40a5d04dcd8a --- /dev/null +++ b/src/core/server/saved_objects/service/lib/create_repository.test.ts @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObjectsRepository } from './repository'; +import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; +import { SavedObjectsSchema } from '../../schema'; +import { KibanaMigrator } from '../../migrations'; +import { Config } from 'src/core/server/config'; +import { createRepository } from './create_repository'; +jest.mock('./repository'); + +describe('#createRepository', () => { + const callAdminCluster = jest.fn(); + const schema = new SavedObjectsSchema({ + nsAgnosticType: { isNamespaceAgnostic: true }, + nsType: { indexPattern: 'beats', isNamespaceAgnostic: false }, + hiddenType: { isNamespaceAgnostic: true, hidden: true }, + }); + const mappings = [ + { + pluginId: 'testplugin', + properties: { + nsAgnosticType: { + properties: { + name: { type: 'keyword' }, + }, + }, + nsType: { + properties: { + name: { type: 'keyword' }, + }, + }, + hiddenType: { + properties: { + name: { type: 'keyword' }, + }, + }, + }, + }, + ]; + const migrator = mockKibanaMigrator.create({ savedObjectMappings: mappings }); + const RepositoryConstructor = (SavedObjectsRepository as unknown) as jest.Mock< + SavedObjectsRepository + >; + + beforeEach(() => { + RepositoryConstructor.mockClear(); + }); + + it('should not allow a repository with an undefined type', () => { + try { + createRepository( + (migrator as unknown) as KibanaMigrator, + schema, + {} as Config, + '.kibana-test', + callAdminCluster, + ['unMappedType1', 'unmappedType2'] + ); + } catch (e) { + expect(e).toMatchInlineSnapshot( + `[Error: Missing mappings for saved objects types: 'unMappedType1, unmappedType2']` + ); + } + }); + + it('should create a repository without hidden types', () => { + const repository = createRepository( + (migrator as unknown) as KibanaMigrator, + schema, + {} as Config, + '.kibana-test', + callAdminCluster + ); + expect(repository).toBeDefined(); + expect(RepositoryConstructor.mock.calls[0][0].allowedTypes).toMatchInlineSnapshot(` + Array [ + "config", + "nsAgnosticType", + "nsType", + ] + `); + }); + + it('should create a repository with a unique list of hidden types', () => { + const repository = createRepository( + (migrator as unknown) as KibanaMigrator, + schema, + {} as Config, + '.kibana-test', + callAdminCluster, + ['hiddenType', 'hiddenType', 'hiddenType'] + ); + expect(repository).toBeDefined(); + expect(RepositoryConstructor.mock.calls[0][0].allowedTypes).toMatchInlineSnapshot(` + Array [ + "config", + "nsAgnosticType", + "nsType", + "hiddenType", + ] + `); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/create_repository.ts b/src/core/server/saved_objects/service/lib/create_repository.ts new file mode 100644 index 0000000000000..a0e920a95c2c6 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/create_repository.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { APICaller } from 'src/core/server/elasticsearch'; +import { Config } from 'src/core/server/config'; +import { retryCallCluster } from '../../../elasticsearch/retry_call_cluster'; +import { KibanaMigrator } from '../../migrations'; +import { SavedObjectsSchema } from '../../schema'; +import { getRootPropertiesObjects } from '../../mappings'; +import { SavedObjectsSerializer } from '../../serialization'; +import { SavedObjectsRepository } from '.'; + +export const createRepository = ( + migrator: KibanaMigrator, + schema: SavedObjectsSchema, + config: Config, + indexName: string, + callCluster: APICaller, + extraTypes: string[] = [] +) => { + const mappings = migrator.getActiveMappings(); + const allTypes = Object.keys(getRootPropertiesObjects(mappings)); + const serializer = new SavedObjectsSerializer(schema); + const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type)); + + const missingTypeMappings = extraTypes.filter(type => !allTypes.includes(type)); + if (missingTypeMappings.length > 0) { + throw new Error( + `Missing mappings for saved objects types: '${missingTypeMappings.join(', ')}'` + ); + } + + const allowedTypes = [...new Set(visibleTypes.concat(extraTypes))]; + + return new SavedObjectsRepository({ + index: indexName, + config, + migrator, + mappings, + schema, + serializer, + allowedTypes, + callCluster: retryCallCluster(callCluster), + }); +}; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 4bc159e17ec0f..afac50a680ed5 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -17,13 +17,19 @@ * under the License. */ -export { SavedObjectsRepository, SavedObjectsRepositoryOptions } from './repository'; +export { + ISavedObjectsRepository, + SavedObjectsRepository, + SavedObjectsRepositoryOptions, +} from './repository'; + export { SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, ISavedObjectsClientProvider, SavedObjectsClientProvider, SavedObjectsClientProviderOptions, + SavedObjectsClientFactory, } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts new file mode 100644 index 0000000000000..e69c0ff37d1be --- /dev/null +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISavedObjectsRepository } from './repository'; + +const create = (): jest.Mocked => ({ + create: jest.fn(), + bulkCreate: jest.fn(), + bulkUpdate: jest.fn(), + delete: jest.fn(), + bulkGet: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), + deleteByNamespace: jest.fn(), + incrementCounter: jest.fn(), +}); + +export const savedObjectsRepositoryMock = { create }; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 3d81c2c2efd52..07ad3494ab78c 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -16,14 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - -import { delay } from 'bluebird'; import _ from 'lodash'; import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; import { SavedObjectsErrorHelpers } from './errors'; -import * as legacyElasticsearch from 'elasticsearch'; import { SavedObjectsSchema } from '../../schema'; import { SavedObjectsSerializer } from '../../serialization'; import { getRootPropertiesObjects } from '../../mappings/lib/get_root_properties_objects'; @@ -36,7 +33,6 @@ jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); describe('SavedObjectsRepository', () => { let callAdminCluster; - let onBeforeWrite; let savedObjectsRepository; let migrator; const mockTimestamp = '2017-08-14T15:49:14.886Z'; @@ -254,7 +250,6 @@ describe('SavedObjectsRepository', () => { beforeEach(() => { callAdminCluster = jest.fn(); - onBeforeWrite = jest.fn(); migrator = { migrateDocument: jest.fn(doc => doc), runMigrations: async () => ({ status: 'skipped' }), @@ -272,7 +267,6 @@ describe('SavedObjectsRepository', () => { schema, serializer, allowedTypes, - onBeforeWrite, }); savedObjectsRepository._getCurrentTime = jest.fn(() => mockTimestamp); @@ -350,7 +344,6 @@ describe('SavedObjectsRepository', () => { expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledWith('index', expect.any(Object)); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('should use default index', async () => { @@ -359,8 +352,8 @@ describe('SavedObjectsRepository', () => { title: 'Logstash', }); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); - expect(onBeforeWrite).toHaveBeenCalledWith( + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster).toHaveBeenCalledWith( 'index', expect.objectContaining({ index: '.kibana-test', @@ -374,8 +367,8 @@ describe('SavedObjectsRepository', () => { title: 'Logstash', }); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); - expect(onBeforeWrite).toHaveBeenCalledWith( + expect(callAdminCluster).toHaveBeenCalledTimes(1); + expect(callAdminCluster).toHaveBeenCalledWith( 'index', expect.objectContaining({ index: 'beats', @@ -447,7 +440,6 @@ describe('SavedObjectsRepository', () => { expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledWith('create', expect.any(Object)); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('allows for id to be provided', async () => { @@ -466,8 +458,6 @@ describe('SavedObjectsRepository', () => { id: 'index-pattern:logstash-*', }) ); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('self-generates an ID', async () => { @@ -482,8 +472,6 @@ describe('SavedObjectsRepository', () => { id: expect.objectContaining(/index-pattern:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/), }) ); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('prepends namespace to the id and adds namespace to body when providing namespace for namespaced type', async () => { @@ -510,7 +498,6 @@ describe('SavedObjectsRepository', () => { }), }) ); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { @@ -535,7 +522,6 @@ describe('SavedObjectsRepository', () => { }), }) ); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { @@ -561,7 +547,6 @@ describe('SavedObjectsRepository', () => { }), }) ); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('defaults to empty references array if none are provided', async () => { @@ -658,8 +643,6 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'ref_0', type: 'test', id: '2' }], }, ]); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('defaults to a refresh setting of `wait_for`', async () => { @@ -821,10 +804,7 @@ describe('SavedObjectsRepository', () => { }) ); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); - callAdminCluster.mockReset(); - onBeforeWrite.mockReset(); callAdminCluster.mockReturnValue({ items: [{ create: { type: 'foo', id: 'bar', _primary_term: 1, _seq_no: 1 } }], @@ -844,8 +824,6 @@ describe('SavedObjectsRepository', () => { ], }) ); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('mockReturnValue document errors', async () => { @@ -997,7 +975,6 @@ describe('SavedObjectsRepository', () => { ], }) ); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { @@ -1044,7 +1021,6 @@ describe('SavedObjectsRepository', () => { ], }) ); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { @@ -1072,7 +1048,6 @@ describe('SavedObjectsRepository', () => { ], }) ); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('should return objects in the same order regardless of type', () => { }); @@ -1116,8 +1091,6 @@ describe('SavedObjectsRepository', () => { index: '.kibana-test', ignore: [404], }); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id when providing no namespace for namespaced type`, async () => { @@ -1131,8 +1104,6 @@ describe('SavedObjectsRepository', () => { index: '.kibana-test', ignore: [404], }); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id when providing namespace for namespace agnostic type`, async () => { @@ -1148,8 +1119,6 @@ describe('SavedObjectsRepository', () => { index: '.kibana-test', ignore: [404], }); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('defaults to a refresh setting of `wait_for`', async () => { @@ -1180,7 +1149,6 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockReturnValue(deleteByQueryResults); expect(savedObjectsRepository.deleteByNamespace()).rejects.toThrowErrorMatchingSnapshot(); expect(callAdminCluster).not.toHaveBeenCalled(); - expect(onBeforeWrite).not.toHaveBeenCalled(); }); it('requires namespace to be a string', async () => { @@ -1189,7 +1157,6 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository.deleteByNamespace(['namespace-1', 'namespace-2']) ).rejects.toThrowErrorMatchingSnapshot(); expect(callAdminCluster).not.toHaveBeenCalled(); - expect(onBeforeWrite).not.toHaveBeenCalled(); }); it('constructs a deleteByQuery call using all types that are namespace aware', async () => { @@ -1198,7 +1165,6 @@ describe('SavedObjectsRepository', () => { expect(result).toEqual(deleteByQueryResults); expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, schema, { namespace: 'my-namespace', @@ -1247,7 +1213,6 @@ describe('SavedObjectsRepository', () => { it('requires type to be defined', async () => { await expect(savedObjectsRepository.find({})).rejects.toThrow(/options\.type must be/); expect(callAdminCluster).not.toHaveBeenCalled(); - expect(onBeforeWrite).not.toHaveBeenCalled(); }); it('requires searchFields be an array if defined', async () => { @@ -1257,7 +1222,6 @@ describe('SavedObjectsRepository', () => { throw new Error('expected find() to reject'); } catch (error) { expect(callAdminCluster).not.toHaveBeenCalled(); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(error.message).toMatch('must be an array'); } }); @@ -1269,7 +1233,6 @@ describe('SavedObjectsRepository', () => { throw new Error('expected find() to reject'); } catch (error) { expect(callAdminCluster).not.toHaveBeenCalled(); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(error.message).toMatch('must be an array'); } }); @@ -1371,7 +1334,6 @@ describe('SavedObjectsRepository', () => { getSearchDslNS.getSearchDsl.mockReturnValue({ query: 1, aggregations: 2 }); await savedObjectsRepository.find({ type: 'foo' }); expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(callAdminCluster).toHaveBeenCalledWith( 'search', expect.objectContaining({ @@ -1440,8 +1402,6 @@ describe('SavedObjectsRepository', () => { from: 50, }) ); - - expect(onBeforeWrite).not.toHaveBeenCalled(); }); it('can filter by fields', async () => { @@ -1463,8 +1423,6 @@ describe('SavedObjectsRepository', () => { ], }) ); - - expect(onBeforeWrite).not.toHaveBeenCalled(); }); it('should set rest_total_hits_as_int to true on a request', async () => { @@ -1516,7 +1474,6 @@ describe('SavedObjectsRepository', () => { it('formats Elasticsearch response when there is no namespace', async () => { callAdminCluster.mockResolvedValue(noNamespaceResult); const response = await savedObjectsRepository.get('index-pattern', 'logstash-*'); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(response).toEqual({ id: 'logstash-*', type: 'index-pattern', @@ -1532,7 +1489,6 @@ describe('SavedObjectsRepository', () => { it('formats Elasticsearch response when there are namespaces', async () => { callAdminCluster.mockResolvedValue(namespacedResult); const response = await savedObjectsRepository.get('index-pattern', 'logstash-*'); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(response).toEqual({ id: 'logstash-*', type: 'index-pattern', @@ -1551,7 +1507,6 @@ describe('SavedObjectsRepository', () => { namespace: 'foo-namespace', }); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledWith( expect.any(String), @@ -1565,7 +1520,6 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockResolvedValue(noNamespaceResult); await savedObjectsRepository.get('index-pattern', 'logstash-*'); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledWith( expect.any(String), @@ -1581,7 +1535,6 @@ describe('SavedObjectsRepository', () => { namespace: 'foo-namespace', }); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledWith( expect.any(String), @@ -1630,8 +1583,6 @@ describe('SavedObjectsRepository', () => { }, }) ); - - expect(onBeforeWrite).not.toHaveBeenCalled(); }); it('prepends namespace and type appropriately to id when getting objects when there is a namespace', async () => { @@ -1661,8 +1612,6 @@ describe('SavedObjectsRepository', () => { }, }) ); - - expect(onBeforeWrite).not.toHaveBeenCalled(); }); it('mockReturnValue early for empty objects argument', async () => { @@ -1672,7 +1621,6 @@ describe('SavedObjectsRepository', () => { expect(response.saved_objects).toHaveLength(0); expect(callAdminCluster).not.toHaveBeenCalled(); - expect(onBeforeWrite).not.toHaveBeenCalled(); }); it('handles missing ids gracefully', async () => { @@ -1723,7 +1671,6 @@ describe('SavedObjectsRepository', () => { { id: 'bad', type: 'config' }, ]); - expect(onBeforeWrite).not.toHaveBeenCalled(); expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(savedObjects).toHaveLength(2); @@ -1991,8 +1938,6 @@ describe('SavedObjectsRepository', () => { refresh: 'wait_for', index: '.kibana-test', }); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { @@ -2033,8 +1978,6 @@ describe('SavedObjectsRepository', () => { refresh: 'wait_for', index: '.kibana-test', }); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { @@ -2076,8 +2019,6 @@ describe('SavedObjectsRepository', () => { refresh: 'wait_for', index: '.kibana-test', }); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('defaults to a refresh setting of `wait_for`', async () => { @@ -2504,8 +2445,6 @@ describe('SavedObjectsRepository', () => { }, ], }); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { @@ -2562,8 +2501,6 @@ describe('SavedObjectsRepository', () => { }, ], }); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { @@ -2723,8 +2660,6 @@ describe('SavedObjectsRepository', () => { expect(requestDoc.body.script.params.type).toBe('config'); expect(requestDoc.body.upsert.type).toBe('config'); expect(requestDoc).toHaveProperty('body.upsert.config'); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => { @@ -2737,8 +2672,6 @@ describe('SavedObjectsRepository', () => { expect(requestDoc.body.script.params.type).toBe('config'); expect(requestDoc.body.upsert.type).toBe('config'); expect(requestDoc).toHaveProperty('body.upsert.config'); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { @@ -2769,8 +2702,6 @@ describe('SavedObjectsRepository', () => { expect(requestDoc.body.script.params.type).toBe('globaltype'); expect(requestDoc.body.upsert.type).toBe('globaltype'); expect(requestDoc).toHaveProperty('body.upsert.globaltype'); - - expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); it('should assert that the "type" and "counterFieldName" arguments are strings', () => { @@ -2819,39 +2750,6 @@ describe('SavedObjectsRepository', () => { }); }); - describe('onBeforeWrite', () => { - it('blocks calls to callCluster of requests', async () => { - onBeforeWrite.mockReturnValue(delay(500)); - callAdminCluster.mockReturnValue({ result: 'deleted', found: true }); - - const deletePromise = savedObjectsRepository.delete('foo', 'id'); - await delay(100); - expect(onBeforeWrite).toHaveBeenCalledTimes(1); - expect(callAdminCluster).not.toHaveBeenCalled(); - await deletePromise; - expect(onBeforeWrite).toHaveBeenCalledTimes(1); - expect(callAdminCluster).toHaveBeenCalledTimes(1); - }); - - it('can throw es errors and have them decorated as SavedObjectsClient errors', async () => { - expect.assertions(4); - - const es401 = new legacyElasticsearch.errors[401](); - expect(SavedObjectsErrorHelpers.isNotAuthorizedError(es401)).toBe(false); - onBeforeWrite.mockImplementation(() => { - throw es401; - }); - - try { - await savedObjectsRepository.delete('foo', 'id'); - } catch (error) { - expect(onBeforeWrite).toHaveBeenCalledTimes(1); - expect(error).toBe(es401); - expect(SavedObjectsErrorHelpers.isNotAuthorizedError(error)).toBe(true); - } - }); - }); - describe('types on custom index', () => { it('should error when attempting to \'update\' an unsupported type', async () => { await expect( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e8f1fb16461c1..f9e48aba5a70e 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -19,7 +19,6 @@ import { omit } from 'lodash'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; - import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; @@ -42,7 +41,6 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkUpdateOptions, SavedObjectsDeleteOptions, - SavedObjectsDeleteByNamespaceOptions, } from '../saved_objects_client'; import { SavedObject, @@ -75,6 +73,7 @@ const isLeft = (either: Either): either is Left => { export interface SavedObjectsRepositoryOptions { index: string; + /** @deprecated Will be removed once SavedObjectsSchema is exposed from Core */ config: Config; mappings: IndexMapping; callCluster: CallCluster; @@ -82,17 +81,38 @@ export interface SavedObjectsRepositoryOptions { serializer: SavedObjectsSerializer; migrator: KibanaMigrator; allowedTypes: string[]; - onBeforeWrite?: (...args: Parameters) => Promise; } -export interface IncrementCounterOptions extends SavedObjectsBaseOptions { +/** + * @public + */ +export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { migrationVersion?: SavedObjectsMigrationVersion; /** The Elasticsearch Refresh setting for this operation */ refresh?: MutatingOperationRefreshSetting; } +/** + * + * @public + */ +export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + const DEFAULT_REFRESH_SETTING = 'wait_for'; +/** + * See {@link SavedObjectsRepository} + * + * @public + */ +export type ISavedObjectsRepository = Pick; + +/** + * @public + */ export class SavedObjectsRepository { private _migrator: KibanaMigrator; private _index: string; @@ -100,10 +120,10 @@ export class SavedObjectsRepository { private _mappings: IndexMapping; private _schema: SavedObjectsSchema; private _allowedTypes: string[]; - private _onBeforeWrite: (...args: Parameters) => Promise; private _unwrappedCallCluster: CallCluster; private _serializer: SavedObjectsSerializer; + /** @internal */ constructor(options: SavedObjectsRepositoryOptions) { const { index, @@ -114,7 +134,6 @@ export class SavedObjectsRepository { serializer, migrator, allowedTypes = [], - onBeforeWrite = () => Promise.resolve(), } = options; // It's important that we migrate documents / mark them as up-to-date @@ -134,8 +153,6 @@ export class SavedObjectsRepository { } this._allowedTypes = allowedTypes; - this._onBeforeWrite = onBeforeWrite; - this._unwrappedCallCluster = async (...args: Parameters) => { await migrator.runMigrations(); return callCluster(...args); @@ -805,7 +822,7 @@ export class SavedObjectsRepository { type: string, id: string, counterFieldName: string, - options: IncrementCounterOptions = {} + options: SavedObjectsIncrementCounterOptions = {} ) { if (typeof type !== 'string') { throw new Error('"type" argument must be a string'); @@ -871,7 +888,6 @@ export class SavedObjectsRepository { private async _writeToCluster(...args: Parameters) { try { - await this._onBeforeWrite(...args); return await this._callCluster(...args); } catch (err) { throw decorateEsError(err); diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts index 87607acd94fc4..0b67727455333 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts @@ -55,8 +55,7 @@ export interface SavedObjectsClientProviderOptions { } /** - * @public - * See {@link SavedObjectsClientProvider} + * @internal */ export type ISavedObjectsClientProvider = Pick< SavedObjectsClientProvider, @@ -66,6 +65,8 @@ export type ISavedObjectsClientProvider = Pick< /** * Provider for the Scoped Saved Objects Client. * + * @internal + * * @internalRemarks Because `getClient` is synchronous the Client Provider does * not support creating factories that react to new ES clients emitted from * elasticsearch.adminClient$. The Client Provider therefore doesn't support diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 550e8a1de0d80..b0b2633646e10 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectsRepository } from './lib'; +import { ISavedObjectsRepository } from './lib'; import { SavedObject, SavedObjectAttributes, @@ -126,15 +126,6 @@ export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { refresh?: MutatingOperationRefreshSetting; } -/** - * - * @public - */ -export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions { - /** The Elasticsearch Refresh setting for this operation */ - refresh?: MutatingOperationRefreshSetting; -} - /** * * @public @@ -180,9 +171,10 @@ export class SavedObjectsClient { public static errors = SavedObjectsErrorHelpers; public errors = SavedObjectsErrorHelpers; - private _repository: SavedObjectsRepository; + private _repository: ISavedObjectsRepository; - constructor(repository: SavedObjectsRepository) { + /** @internal */ + constructor(repository: ISavedObjectsRepository) { this._repository = repository; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 3bbcb85fea9e5..411e5636069c1 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -512,11 +512,15 @@ export interface CoreSetup { // (undocumented) http: HttpServiceSetup; // (undocumented) + savedObjects: SavedObjectsServiceSetup; + // (undocumented) uiSettings: UiSettingsServiceSetup; } // @public export interface CoreStart { + // (undocumented) + savedObjects: SavedObjectsServiceStart; } // @public @@ -729,6 +733,9 @@ export interface IRouter { // @public export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; +// @public +export type ISavedObjectsRepository = Pick; + // @public export type IScopedClusterClient = Pick; @@ -1200,8 +1207,8 @@ export interface SavedObjectsBulkUpdateResponse(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; @@ -1219,6 +1226,11 @@ export class SavedObjectsClient { // @public export type SavedObjectsClientContract = Pick; +// @public +export type SavedObjectsClientFactory = ({ request, }: { + request: Request; +}) => SavedObjectsClientContract; + // @public export interface SavedObjectsClientProviderOptions { // (undocumented) @@ -1246,6 +1258,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { refresh?: MutatingOperationRefreshSetting; } +// @public (undocumented) +export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; +} + // @public (undocumented) export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { refresh?: MutatingOperationRefreshSetting; @@ -1456,6 +1473,13 @@ export interface SavedObjectsImportUnsupportedTypeError { type: 'unsupported_type'; } +// @public (undocumented) +export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions { + // (undocumented) + migrationVersion?: SavedObjectsMigrationVersion; + refresh?: MutatingOperationRefreshSetting; +} + // @internal @deprecated (undocumented) export interface SavedObjectsLegacyService { // Warning: (ae-forgotten-export) The symbol "SavedObjectsClientProvider" needs to be exported by the entry point index.d.ts @@ -1515,6 +1539,32 @@ export interface SavedObjectsRawDoc { _type?: string; } +// @public (undocumented) +export class SavedObjectsRepository { + // Warning: (ae-forgotten-export) The symbol "SavedObjectsRepositoryOptions" needs to be exported by the entry point index.d.ts + // + // @internal + constructor(options: SavedObjectsRepositoryOptions); + bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; + bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; + bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; + create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; + delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; + deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; + // (undocumented) + find({ search, defaultSearchOperator, searchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespace, type, filter, }: SavedObjectsFindOptions): Promise>; + get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; + incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise<{ + id: string; + type: string; + updated_at: string; + references: any; + version: string; + attributes: any; + }>; + update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + } + // @public export interface SavedObjectsResolveImportErrorsOptions { // (undocumented) @@ -1555,6 +1605,19 @@ export class SavedObjectsSerializer { savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): SavedObjectsRawDoc; } +// @public +export interface SavedObjectsServiceSetup { + addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; + createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository; + createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository; + setClientFactory: (customClientFactory: SavedObjectsClientFactory) => void; +} + +// @public +export interface SavedObjectsServiceStart { + getScopedClient: (req: KibanaRequest, options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract; +} + // @public (undocumented) export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index f8eb5e32f4c5a..b378273a7075a 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -35,9 +35,11 @@ jest.doMock('./elasticsearch/elasticsearch_service', () => ({ ElasticsearchService: jest.fn(() => mockElasticsearchService), })); -export const mockLegacyService = { +import { ILegacyService } from './legacy/legacy_service'; +export const mockLegacyService: ILegacyService = { legacyId: Symbol(), - setup: jest.fn().mockReturnValue({ uiExports: {} }), + discoverPlugins: jest.fn().mockReturnValue({ uiExports: {} }), + setup: jest.fn(), start: jest.fn(), stop: jest.fn(), }; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 6c38de03f0f2d..b36468b85d7a1 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -38,7 +38,6 @@ import { config as savedObjectsConfig } from './saved_objects'; import { config as uiSettingsConfig } from './ui_settings'; import { mapToObject } from '../utils/'; import { ContextService } from './context'; -import { SavedObjectsServiceSetup } from './saved_objects/saved_objects_service'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup } from './internal_types'; @@ -78,6 +77,7 @@ export class Server { // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. const pluginDependencies = await this.plugins.discover(); + const legacyPlugins = await this.legacy.discoverPlugins(); const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: // 1) Can access context from any NP plugin @@ -103,40 +103,41 @@ export class Server { http: httpSetup, }); + const savedObjectsSetup = await this.savedObjects.setup({ + elasticsearch: elasticsearchServiceSetup, + legacyPlugins, + }); + const coreSetup: InternalCoreSetup = { context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, uiSettings: uiSettingsSetup, + savedObjects: savedObjectsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); - const legacySetup = await this.legacy.setup({ + await this.legacy.setup({ core: { ...coreSetup, plugins: pluginsSetup }, plugins: mapToObject(pluginsSetup.contracts), }); - const savedObjectsSetup = await this.savedObjects.setup({ - elasticsearch: elasticsearchServiceSetup, - legacy: legacySetup, - }); - - this.registerCoreContext(coreSetup, savedObjectsSetup); + this.registerCoreContext(coreSetup); return coreSetup; } public async start() { this.log.debug('starting server'); - const pluginsStart = await this.plugins.start({}); const savedObjectsStart = await this.savedObjects.start({}); + const pluginsStart = await this.plugins.start({ savedObjects: savedObjectsStart }); + const coreStart = { savedObjects: savedObjectsStart, plugins: pluginsStart, }; - await this.legacy.start({ core: coreStart, plugins: mapToObject(pluginsStart.contracts), @@ -164,17 +165,14 @@ export class Server { ); } - private registerCoreContext( - coreSetup: InternalCoreSetup, - savedObjects: SavedObjectsServiceSetup - ) { + private registerCoreContext(coreSetup: InternalCoreSetup) { coreSetup.http.registerRouteHandlerContext( coreId, 'core', async (context, req): Promise => { const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise(); - const savedObjectsClient = savedObjects.clientProvider.getClient(req); + const savedObjectsClient = coreSetup.savedObjects.getScopedClient(req); return { savedObjects: { diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index 6f491783829a8..1d88b7dc4e634 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -26,6 +26,7 @@ import { toArray } from 'rxjs/operators'; import { deleteIndex } from './delete_index'; import { collectUiExports } from '../../../legacy/ui/ui_exports'; import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; +import { SavedObjectsSchema } from '../../../core/server/saved_objects'; import { findPluginSpecs } from '../../../legacy/plugin_discovery'; /** @@ -101,7 +102,7 @@ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { error: log.error.bind(log), }, version: kibanaVersion, - savedObjectSchemas: uiExports.savedObjectSchemas, + savedObjectSchemas: new SavedObjectsSchema(uiExports.savedObjectSchemas), savedObjectMappings: uiExports.savedObjectMappings, savedObjectMigrations: uiExports.savedObjectMigrations, savedObjectValidations: uiExports.savedObjectValidations, From 6b5108a57dce73b01ba1cfad038e58ac11e03bbf Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Wed, 27 Nov 2019 17:22:15 +0100 Subject: [PATCH 116/128] fixes pagination tests (#51822) --- .../smoke_tests/pagination/pagination.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts index 7822f4d30365d..ebd0ad0125efb 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts @@ -23,19 +23,19 @@ describe('Pagination', () => { return logout(); }); - it.skip('pagination updates results and page number', () => { + it('pagination updates results and page number', () => { loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); - cy.get(getDraggableField('user.name')) + cy.get(getDraggableField('process.name')) .first() .invoke('text') .then(text1 => { cy.get(getPageButtonSelector(2)).click({ force: true }); // wait for table to be done loading waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); - cy.get(getDraggableField('user.name')) + cy.get(getDraggableField('process.name')) .first() .invoke('text') .should(text2 => { @@ -55,7 +55,7 @@ describe('Pagination', () => { // wait for table to be done loading waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); - cy.get(getDraggableField('user.name')) + cy.get(getDraggableField('process.name')) .first() .invoke('text') .then(text2 => { @@ -70,7 +70,7 @@ describe('Pagination', () => { waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); // check uncommon processes table picks up at 3 cy.get(getPageButtonSelector(2)).should('have.class', 'euiPaginationButton-isActive'); - cy.get(getDraggableField('user.name')) + cy.get(getDraggableField('process.name')) .first() .invoke('text') .should(text1 => { @@ -82,7 +82,7 @@ describe('Pagination', () => { * We only want to comment this code/test for now because it can be nondeterministic * when we figure out a way to really mock the data, we should come back to it */ - it.skip('pagination resets results and page number to first page when refresh is clicked', () => { + it('pagination resets results and page number to first page when refresh is clicked', () => { loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); cy.get(NUMBERED_PAGINATION, { timeout: DEFAULT_TIMEOUT }); cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); @@ -100,7 +100,7 @@ describe('Pagination', () => { .last() .click({ force: true }); waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); - cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); + cy.get(getPageButtonSelector(2)).should('have.class', 'euiPaginationButton-isActive'); // cy.get(getDraggableField('user.name')) // .first() // .invoke('text') From 7a0940958749bc11660f1517a6b8ac6279ab6591 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 27 Nov 2019 18:40:28 +0200 Subject: [PATCH 117/128] =?UTF-8?q?Move=20errors=20and=20validate=20index?= =?UTF-8?q?=20pattern=20=E2=87=92=20NP=20(#51805)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move errors and validate index pattern to NP * removed unused mock * remvoed irrelevant mock * Removed unneeded mocks * fix test --- src/legacy/core_plugins/data/public/index.ts | 14 +---- .../index_patterns/index_pattern.ts | 4 +- .../index_patterns/index_patterns.test.ts | 4 -- .../index_patterns_api_client.ts | 4 +- .../index_patterns/index_patterns_service.ts | 18 +------ .../data/public/index_patterns/utils.ts | 41 --------------- .../public/index_patterns/__mocks__/index.ts | 14 +---- src/legacy/ui/public/index_patterns/index.ts | 22 ++++---- .../indices/validate/validate_index.test.js | 1 - src/plugins/data/public/index.ts | 1 + .../data/public/index_patterns/errors.ts | 30 +---------- .../data/public/index_patterns/index.ts | 36 +++++++++++++ .../data/public/index_patterns/lib/index.ts | 2 + .../data/public/index_patterns/lib/types.ts | 23 +++++++++ .../lib/validate_index_pattern.test.ts} | 15 +++--- .../lib/validate_index_pattern.ts | 51 +++++++++++++++++++ .../auto_follow_pattern_add.test.js | 1 - .../auto_follow_pattern_edit.test.js | 1 - .../auto_follow_pattern_list.test.js | 1 - .../follower_index_add.test.js | 1 - .../follower_index_edit.test.js | 1 - .../follower_indices_list.test.js | 1 - .../__jest__/client_integration/home.test.js | 1 - .../auto_follow_pattern_form.test.js | 1 - .../auto_follow_pattern_validators.test.js | 1 - .../job_create_clone.test.js | 1 - .../job_create_date_histogram.test.js | 1 - .../job_create_histogram.test.js | 1 - .../job_create_logistics.test.js | 1 - .../job_create_metrics.test.js | 1 - .../job_create_review.test.js | 1 - .../job_create_terms.test.js | 1 - .../client_integration/job_list.test.js | 1 - .../client_integration/job_list_clone.test.js | 1 - 34 files changed, 136 insertions(+), 162 deletions(-) rename src/{legacy/core_plugins => plugins}/data/public/index_patterns/errors.ts (59%) create mode 100644 src/plugins/data/public/index_patterns/index.ts create mode 100644 src/plugins/data/public/index_patterns/lib/types.ts rename src/{legacy/core_plugins/data/public/index_patterns/utils.test.ts => plugins/data/public/index_patterns/lib/validate_index_pattern.test.ts} (79%) create mode 100644 src/plugins/data/public/index_patterns/lib/validate_index_pattern.ts diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 01f67a63ca9be..184084e3cc3e6 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -43,16 +43,4 @@ export { SearchBar, SearchBarProps, SavedQueryAttributes, SavedQuery } from './s /** @public static code */ export * from '../common'; export { FilterStateManager } from './filter/filter_manager'; -export { - CONTAINS_SPACES, - getFromSavedObject, - getRoutes, - validateIndexPattern, - ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, - IndexPatternAlreadyExists, - IndexPatternMissingIndices, - NoDefaultIndexPattern, - NoDefinedIndexPatterns, -} from './index_patterns'; +export { getFromSavedObject, getRoutes } from './index_patterns'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts index f77342c7bc274..de364b6c217dd 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts @@ -32,10 +32,10 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, + indexPatterns, } from '../../../../../../plugins/data/public'; import { findIndexPatternByTitle, getRoutes } from '../utils'; -import { IndexPatternMissingIndices } from '../errors'; import { Field, FieldList, FieldListInterface, FieldType } from '../fields'; import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; @@ -499,7 +499,7 @@ export class IndexPattern implements IIndexPattern { // so do not rethrow the error here const { toasts } = getNotifications(); - if (err instanceof IndexPatternMissingIndices) { + if (err instanceof indexPatterns.IndexPatternMissingIndices) { toasts.addDanger((err as any).message); return []; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index 0a5d1bfcae21f..2ad0a1f1394e5 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -25,10 +25,6 @@ import { HttpServiceBase, } from 'kibana/public'; -jest.mock('../errors', () => ({ - IndexPatternMissingIndices: jest.fn(), -})); - jest.mock('./index_pattern', () => { class IndexPattern { init = async () => { diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index c0e8516a75bb3..87dd7a68e3061 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -18,7 +18,7 @@ */ import { HttpServiceBase } from 'src/core/public'; -import { IndexPatternMissingIndices } from '../errors'; +import { indexPatterns } from '../../../../../../plugins/data/public'; const API_BASE_URL: string = `/api/index_patterns/`; @@ -46,7 +46,7 @@ export class IndexPatternsApiClient { }) .catch((resp: any) => { if (resp.body.statusCode === 404 && resp.body.statuscode === 'no_matching_indices') { - throw new IndexPatternMissingIndices(resp.body.message); + throw new indexPatterns.IndexPatternMissingIndices(resp.body.message); } throw new Error(resp.body.message || resp.body.error || `${resp.body.statusCode} Response`); diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index 381cd491f0210..9973a7081443d 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -89,23 +89,7 @@ export class IndexPatternsService { // static code /** @public */ -export { - CONTAINS_SPACES, - getFromSavedObject, - getRoutes, - ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, - validateIndexPattern, -} from './utils'; - -/** @public */ -export { - IndexPatternAlreadyExists, - IndexPatternMissingIndices, - NoDefaultIndexPattern, - NoDefinedIndexPatterns, -} from './errors'; +export { getFromSavedObject, getRoutes } from './utils'; // types diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.ts b/src/legacy/core_plugins/data/public/index_patterns/utils.ts index 8c2878a3ff9ba..0d0d5705a0ccc 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/utils.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/utils.ts @@ -21,27 +21,6 @@ import { find, get } from 'lodash'; import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; -export const ILLEGAL_CHARACTERS = 'ILLEGAL_CHARACTERS'; -export const CONTAINS_SPACES = 'CONTAINS_SPACES'; -export const INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE = ['\\', '/', '?', '"', '<', '>', '|']; -export const INDEX_PATTERN_ILLEGAL_CHARACTERS = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.concat( - ' ' -); - -function findIllegalCharacters(indexPattern: string): string[] { - const illegalCharacters = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.reduce( - (chars: string[], char: string) => { - if (indexPattern.includes(char)) { - chars.push(char); - } - return chars; - }, - [] - ); - - return illegalCharacters; -} - /** * Returns an object matching a given title * @@ -71,26 +50,6 @@ export async function findIndexPatternByTitle( ); } -function indexPatternContainsSpaces(indexPattern: string): boolean { - return indexPattern.includes(' '); -} - -export function validateIndexPattern(indexPattern: string) { - const errors: Record = {}; - - const illegalCharacters = findIllegalCharacters(indexPattern); - - if (illegalCharacters.length) { - errors[ILLEGAL_CHARACTERS] = illegalCharacters; - } - - if (indexPatternContainsSpaces(indexPattern)) { - errors[CONTAINS_SPACES] = true; - } - - return errors; -} - export function getFromSavedObject(savedObject: any) { if (get(savedObject, 'attributes.fields') === undefined) { return; diff --git a/src/legacy/ui/public/index_patterns/__mocks__/index.ts b/src/legacy/ui/public/index_patterns/__mocks__/index.ts index 145045a90ade8..9ff09835e48da 100644 --- a/src/legacy/ui/public/index_patterns/__mocks__/index.ts +++ b/src/legacy/ui/public/index_patterns/__mocks__/index.ts @@ -31,16 +31,4 @@ export const { } = dataSetup.indexPatterns!; // static code -export { - CONTAINS_SPACES, - getFromSavedObject, - getRoutes, - validateIndexPattern, - ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, - IndexPatternAlreadyExists, - IndexPatternMissingIndices, - NoDefaultIndexPattern, - NoDefinedIndexPatterns, -} from '../../../../core_plugins/data/public'; +export { getFromSavedObject, getRoutes } from '../../../../core_plugins/data/public'; diff --git a/src/legacy/ui/public/index_patterns/index.ts b/src/legacy/ui/public/index_patterns/index.ts index d0ff0aaa8c72c..06001667c9e53 100644 --- a/src/legacy/ui/public/index_patterns/index.ts +++ b/src/legacy/ui/public/index_patterns/index.ts @@ -32,20 +32,16 @@ export const { formatHitProvider, } = data.indexPatterns; +import { indexPatterns } from '../../../../plugins/data/public'; + // static code -export { - CONTAINS_SPACES, - getFromSavedObject, - getRoutes, - validateIndexPattern, - ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, - IndexPatternAlreadyExists, - IndexPatternMissingIndices, - NoDefaultIndexPattern, - NoDefinedIndexPatterns, -} from '../../../core_plugins/data/public'; +export { getFromSavedObject, getRoutes } from '../../../core_plugins/data/public'; + +export const INDEX_PATTERN_ILLEGAL_CHARACTERS = indexPatterns.ILLEGAL_CHARACTERS; +export const INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE = indexPatterns.ILLEGAL_CHARACTERS_VISIBLE; +export const ILLEGAL_CHARACTERS = indexPatterns.ILLEGAL_CHARACTERS_KEY; +export const CONTAINS_SPACES = indexPatterns.CONTAINS_SPACES_KEY; +export const validateIndexPattern = indexPatterns.validate; // types export { diff --git a/src/legacy/ui/public/indices/validate/validate_index.test.js b/src/legacy/ui/public/indices/validate/validate_index.test.js index 62a6c8610fd40..f81ba9d4bcab5 100644 --- a/src/legacy/ui/public/indices/validate/validate_index.test.js +++ b/src/legacy/ui/public/indices/validate/validate_index.test.js @@ -18,7 +18,6 @@ */ jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../constants'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index ace0b44378b45..eca6258099141 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -30,6 +30,7 @@ export * from '../common'; export * from './autocomplete_provider'; export * from './field_formats_provider'; +export * from './index_patterns'; export * from './types'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/errors.ts b/src/plugins/data/public/index_patterns/errors.ts similarity index 59% rename from src/legacy/core_plugins/data/public/index_patterns/errors.ts rename to src/plugins/data/public/index_patterns/errors.ts index c64da47b8c785..3eb43eaf460cc 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/errors.ts +++ b/src/plugins/data/public/index_patterns/errors.ts @@ -19,17 +19,7 @@ /* eslint-disable */ -import { KbnError } from '../../../../../plugins/kibana_utils/public'; - -/** - * when a mapping already exists for a field the user is attempting to add - * @param {String} name - the field name - */ -export class IndexPatternAlreadyExists extends KbnError { - constructor(name: string) { - super(`An index pattern of "${name}" already exists`); - } -} +import { KbnError } from '../../../kibana_utils/public'; /** * Tried to call a method that relies on SearchSource having an indexPattern assigned @@ -43,21 +33,3 @@ export class IndexPatternMissingIndices extends KbnError { ); } } - -/** - * Tried to call a method that relies on SearchSource having an indexPattern assigned - */ -export class NoDefinedIndexPatterns extends KbnError { - constructor() { - super('Define at least one index pattern to continue'); - } -} - -/** - * Tried to load a route besides management/kibana/index but you don't have a default index pattern! - */ -export class NoDefaultIndexPattern extends KbnError { - constructor() { - super('Please specify a default index pattern'); - } -} diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts new file mode 100644 index 0000000000000..aedfc18db3ade --- /dev/null +++ b/src/plugins/data/public/index_patterns/index.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPatternMissingIndices } from './errors'; +import { + ILLEGAL_CHARACTERS_KEY, + CONTAINS_SPACES_KEY, + ILLEGAL_CHARACTERS_VISIBLE, + ILLEGAL_CHARACTERS, + validateIndexPattern, +} from './lib'; + +export const indexPatterns = { + ILLEGAL_CHARACTERS_KEY, + CONTAINS_SPACES_KEY, + ILLEGAL_CHARACTERS_VISIBLE, + ILLEGAL_CHARACTERS, + IndexPatternMissingIndices, + validate: validateIndexPattern, +}; diff --git a/src/plugins/data/public/index_patterns/lib/index.ts b/src/plugins/data/public/index_patterns/lib/index.ts index d1c229513aa33..3b87d91bb9fff 100644 --- a/src/plugins/data/public/index_patterns/lib/index.ts +++ b/src/plugins/data/public/index_patterns/lib/index.ts @@ -18,3 +18,5 @@ */ export { getIndexPatternTitle } from './get_index_pattern_title'; +export * from './types'; +export { validateIndexPattern } from './validate_index_pattern'; diff --git a/src/plugins/data/public/index_patterns/lib/types.ts b/src/plugins/data/public/index_patterns/lib/types.ts new file mode 100644 index 0000000000000..5eb309a1e5a9c --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/types.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const ILLEGAL_CHARACTERS_KEY = 'ILLEGAL_CHARACTERS'; +export const CONTAINS_SPACES_KEY = 'CONTAINS_SPACES'; +export const ILLEGAL_CHARACTERS_VISIBLE = ['\\', '/', '?', '"', '<', '>', '|']; +export const ILLEGAL_CHARACTERS = ILLEGAL_CHARACTERS_VISIBLE.concat(' '); diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.test.ts b/src/plugins/data/public/index_patterns/lib/validate_index_pattern.test.ts similarity index 79% rename from src/legacy/core_plugins/data/public/index_patterns/utils.test.ts rename to src/plugins/data/public/index_patterns/lib/validate_index_pattern.test.ts index cff48144489f0..74e420ffeb5c0 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/utils.test.ts +++ b/src/plugins/data/public/index_patterns/lib/validate_index_pattern.test.ts @@ -17,24 +17,21 @@ * under the License. */ -import { - CONTAINS_SPACES, - ILLEGAL_CHARACTERS, - INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, - validateIndexPattern, -} from './utils'; +import { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_KEY, ILLEGAL_CHARACTERS_VISIBLE } from './types'; + +import { validateIndexPattern } from './validate_index_pattern'; describe('Index Pattern Utils', () => { describe('Validation', () => { it('should not allow space in the pattern', () => { const errors = validateIndexPattern('my pattern'); - expect(errors[CONTAINS_SPACES]).toBe(true); + expect(errors[CONTAINS_SPACES_KEY]).toBe(true); }); it('should not allow illegal characters', () => { - INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.forEach(char => { + ILLEGAL_CHARACTERS_VISIBLE.forEach(char => { const errors = validateIndexPattern(`pattern${char}`); - expect(errors[ILLEGAL_CHARACTERS]).toEqual([char]); + expect(errors[ILLEGAL_CHARACTERS_KEY]).toEqual([char]); }); }); diff --git a/src/plugins/data/public/index_patterns/lib/validate_index_pattern.ts b/src/plugins/data/public/index_patterns/lib/validate_index_pattern.ts new file mode 100644 index 0000000000000..70f5971c91bd5 --- /dev/null +++ b/src/plugins/data/public/index_patterns/lib/validate_index_pattern.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ILLEGAL_CHARACTERS_VISIBLE, CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_KEY } from './types'; + +function indexPatternContainsSpaces(indexPattern: string): boolean { + return indexPattern.includes(' '); +} + +function findIllegalCharacters(indexPattern: string): string[] { + const illegalCharacters = ILLEGAL_CHARACTERS_VISIBLE.reduce((chars: string[], char: string) => { + if (indexPattern.includes(char)) { + chars.push(char); + } + return chars; + }, []); + + return illegalCharacters; +} + +export function validateIndexPattern(indexPattern: string) { + const errors: Record = {}; + + const illegalCharacters = findIllegalCharacters(indexPattern); + + if (illegalCharacters.length) { + errors[ILLEGAL_CHARACTERS_KEY] = illegalCharacters; + } + + if (indexPatternContainsSpaces(indexPattern)) { + errors[CONTAINS_SPACES_KEY] = true; + } + + return errors; +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js index 476f01940d892..7359a24098186 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js @@ -8,7 +8,6 @@ import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './help import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../../../src/legacy/ui/public/index_patterns'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); const { setup } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js index 9ef412883522a..03155f5f55000 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js @@ -9,7 +9,6 @@ import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { AUTO_FOLLOW_PATTERN_EDIT } from './helpers/constants'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); const { setup } = pageHelpers.autoFollowPatternEdit; const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js index 8a6d382190945..904434e46dee0 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -9,7 +9,6 @@ import { setupEnvironment, pageHelpers, nextTick, findTestSubject, getRandomStri import { getAutoFollowPatternClientMock } from '../../fixtures/auto_follow_pattern'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('ui/chrome', () => ({ addBasePath: () => 'api/cross_cluster_replication', diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js index d28d671fb2ace..0d90d4cf3d272 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js @@ -10,7 +10,6 @@ import { RemoteClustersFormField } from '../../public/app/components'; import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../../../src/legacy/ui/public/index_patterns'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); const { setup } = pageHelpers.followerIndexAdd; const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js index 5e74d923d3af5..de1426bf4b72f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js @@ -10,7 +10,6 @@ import { FollowerIndexForm } from '../../public/app/components/follower_index_fo import { FOLLOWER_INDEX_EDIT } from './helpers/constants'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); const { setup } = pageHelpers.followerIndexEdit; const { setup: setupFollowerIndexAdd } = pageHelpers.followerIndexAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js index 6aef850672179..13adea4592534 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js @@ -9,7 +9,6 @@ import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './help import { getFollowerIndexMock } from '../../fixtures/follower_index'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('ui/chrome', () => ({ addBasePath: () => 'api/cross_cluster_replication', diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js index 35ec99846990a..5691ff3a8bc3b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js @@ -8,7 +8,6 @@ import { setupEnvironment, pageHelpers, nextTick } from './helpers'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); const { setup } = pageHelpers.home; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js index f6cc9cb3742ea..eda275ba50c1a 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js @@ -12,7 +12,6 @@ jest.mock('../services/auto_follow_pattern_validators', () => ({ })); jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); describe(' { describe('updateFormErrors()', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js index f70caf2f8080b..6c1d5c8ce171c 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js @@ -8,7 +8,6 @@ import { validateAutoFollowPattern } from './auto_follow_pattern_validators'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); describe('Auto-follow pattern validators', () => { describe('validateAutoFollowPattern()', () => { diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js index 29d2d00163ad8..204bab5c497be 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_clone.test.js @@ -8,7 +8,6 @@ import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { JOB_TO_CLONE, JOB_CLONE_INDEX_PATTERN_CHECK } from './helpers/constants'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js index 59814474396fe..b7b555d986597 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js @@ -9,7 +9,6 @@ import moment from 'moment-timezone'; import { setupEnvironment, pageHelpers } from './helpers'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js index 09417fa8ed307..dbbd7501b1518 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js @@ -7,7 +7,6 @@ import { setupEnvironment, pageHelpers } from './helpers'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js index 99a0aa0935152..a853ef36e01cd 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js @@ -9,7 +9,6 @@ import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../../../src/ import { setupEnvironment, pageHelpers } from './helpers'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js index 2f26d2a7475de..d2f63983a3e36 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js @@ -7,7 +7,6 @@ import { setupEnvironment, pageHelpers } from './helpers'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js index 8ca736e62be7f..c89d37f4e0ac3 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js @@ -9,7 +9,6 @@ import { first } from 'lodash'; import { JOBS } from './helpers/constants'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_terms.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_terms.test.js index 78e8d9ec0c53a..c27b9d0e4ef0f 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_terms.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_terms.test.js @@ -7,7 +7,6 @@ import { setupEnvironment, pageHelpers } from './helpers'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js index 05272bf222612..db7dddad4e3c1 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js @@ -9,7 +9,6 @@ import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { JOBS } from './helpers/constants'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('../../public/crud_app/services', () => { const services = require.requireActual('../../public/crud_app/services'); diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js index ce62f6c67ae03..6feabe7f772ee 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list_clone.test.js @@ -10,7 +10,6 @@ import { getRouter } from '../../public/crud_app/services/routing'; import { CRUD_APP_BASE_PATH } from '../../public/crud_app/constants'; jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); jest.mock('lodash/function/debounce', () => fn => fn); From 2ce0f789bbfd61850d555a609e32e681af5653ef Mon Sep 17 00:00:00 2001 From: Andrea Del Rio Date: Wed, 27 Nov 2019 12:32:49 -0800 Subject: [PATCH 118/128] [Monitoring] Sass cleanup (#51100) --- .../public/components/chart/_chart.scss | 15 +- .../components/chart/horizontal_legend.js | 72 +- .../chart/monitoring_timeseries_container.js | 33 +- .../elasticsearch/indices/indices.js | 65 +- .../components/elasticsearch/nodes/nodes.js | 169 +- .../shard_allocation/_shard_allocation.scss | 89 +- .../shard_allocation/components/assigned.js | 56 +- .../shard_allocation/components/unassigned.js | 34 +- .../shard_allocation/shard_allocation.js | 2 +- .../public/components/status_icon/index.js | 19 +- .../__snapshots__/summary_status.test.js.snap | 38 +- .../public/components/table/_table.scss | 5 + .../nodes/__tests__/get_node_summary.js | 52 +- .../__tests__/get_node_type_class_label.js | 8 +- .../handle_response.test.js.snap | 16 +- .../__snapshots__/map_nodes_info.test.js.snap | 4 +- .../server/lib/elasticsearch/nodes/lookups.js | 30 +- .../fixtures/node_detail_advanced.json | 3126 +++++++++-------- .../fixtures/nodes_listing_cgroup.json | 8 +- .../fixtures/nodes_listing_green.json | 8 +- .../fixtures/nodes_listing_red.json | 8 +- 21 files changed, 1957 insertions(+), 1900 deletions(-) diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/_chart.scss b/x-pack/legacy/plugins/monitoring/public/components/chart/_chart.scss index d3b705a5eb492..1b8ebb762533d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/_chart.scss +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/_chart.scss @@ -1,7 +1,5 @@ -@mixin monitoringNoUserSelect(){ +@mixin monitoringNoUserSelect { user-select: none; - -webkit-touch-callout: none; - -webkit-tap-highlight-color: transparent; } .monRhythmChart__wrapper .monRhythmChart__zoom { @@ -12,7 +10,7 @@ .monRhythmChart__wrapper:hover .monRhythmChart__zoom { visibility: visible; } - + .monRhythmChart { position: relative; display: flex; @@ -50,7 +48,7 @@ // SASSTODO: generic selector div { - @include monitoringNoUserSelect(); + @include monitoringNoUserSelect; } } @@ -58,6 +56,9 @@ font-size: $euiFontSizeXS; cursor: pointer; color: $euiTextColor; + display: flex; + flex-direction: row; + align-items: center; &-isDisabled { opacity: 0.5; @@ -71,7 +72,11 @@ .monRhythmChart__legendLabel { overflow: hidden; white-space: nowrap; + display: flex; + flex-direction: row; + align-items: center; } + .monRhythmChart__legendValue { overflow: hidden; white-space: nowrap; diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/horizontal_legend.js b/x-pack/legacy/plugins/monitoring/public/components/chart/horizontal_legend.js index 9ce4d6224c45e..ab322324ac200 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/horizontal_legend.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/horizontal_legend.js @@ -6,9 +6,7 @@ import React from 'react'; import { includes, isFunction } from 'lodash'; -import { - EuiKeyboardAccessible, -} from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiIcon, EuiKeyboardAccessible } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -23,11 +21,7 @@ export class HorizontalLegend extends React.Component { * @param {Number} value Final value to display */ displayValue(value) { - return ( - - { value } - - ); + return {value}; } /** @@ -44,10 +38,12 @@ export class HorizontalLegend extends React.Component { */ formatter(value, row) { if (!this.validValue(value)) { - return (); + return ( + + ); } if (row && row.tickFormatter) { @@ -61,38 +57,38 @@ export class HorizontalLegend extends React.Component { } createSeries(row, rowIdx) { - const classes = ['col-md-4 col-xs-6 monRhythmChart__legendItem']; + const classes = ['monRhythmChart__legendItem']; if (!includes(this.props.seriesFilter, row.id)) { classes.push('monRhythmChart__legendItem-isDisabled'); } if (!row.label || row.legend === false) { - return ( -
    - ); + return
    ; } return ( -
    this.props.onToggle(event, row.id)} - > - - - { ' ' + row.label + ' ' } - - { this.formatter(this.props.seriesValues[row.id], row) } -
    + + +
    ); } @@ -102,9 +98,9 @@ export class HorizontalLegend extends React.Component { return (
    -
    - { rows } -
    + + {rows} +
    ); } diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js b/x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js index 9216ac7c28705..6760a037fbe8a 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js @@ -12,12 +12,18 @@ import { MonitoringTimeseries } from './monitoring_timeseries'; import { InfoTooltip } from './info_tooltip'; import { - EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiScreenReaderOnly, EuiTextAlign, EuiButtonEmpty + EuiIconTip, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiScreenReaderOnly, + EuiTextAlign, + EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -const zoomOutBtn = (zoomInfo) => { +const zoomOutBtn = zoomInfo => { if (!zoomInfo || !zoomInfo.showZoomOutBtn()) { return null; } @@ -28,9 +34,9 @@ const zoomOutBtn = (zoomInfo) => { - {' '} `${item.metric.label}: ${item.metric.description}`)); + bucketSize, + }, + }), + ].concat(series.map(item => `${item.metric.label}: ${item.metric.description}`)); return ( @@ -68,7 +73,8 @@ export function MonitoringTimeseriesContainer({ series, onBrush, zoomInfo }) {

    - { getTitle(series) }{ units ? ` (${units})` : '' } + {getTitle(series)} + {units ? ` (${units})` : ''} } + content={} /> @@ -95,14 +101,11 @@ export function MonitoringTimeseriesContainer({ series, onBrush, zoomInfo }) { - { zoomOutBtn(zoomInfo) } + {zoomOutBtn(zoomInfo)} - + ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js index d53f267865232..232815e930388 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js @@ -31,12 +31,9 @@ const columns = [ field: 'name', width: '350px', sortable: true, - render: (value) => ( + render: value => (
    - + {value}
    @@ -48,12 +45,13 @@ const columns = [ }), field: 'status', sortable: true, - render: (value) => ( -
    -   + render: value => ( +
    + +   {capitalize(value)}
    - ) + ), }, { name: i18n.translate('xpack.monitoring.elasticsearch.indices.documentCountTitle', { @@ -62,10 +60,8 @@ const columns = [ field: 'doc_count', sortable: true, render: value => ( -
    - {formatMetric(value, LARGE_ABBREVIATED)} -
    - ) +
    {formatMetric(value, LARGE_ABBREVIATED)}
    + ), }, { name: i18n.translate('xpack.monitoring.elasticsearch.indices.dataTitle', { @@ -73,11 +69,7 @@ const columns = [ }), field: 'data_size', sortable: true, - render: value => ( -
    - {formatMetric(value, LARGE_BYTES)} -
    - ) + render: value =>
    {formatMetric(value, LARGE_BYTES)}
    , }, { name: i18n.translate('xpack.monitoring.elasticsearch.indices.indexRateTitle', { @@ -85,11 +77,7 @@ const columns = [ }), field: 'index_rate', sortable: true, - render: value => ( -
    - {formatMetric(value, LARGE_FLOAT, '/s')} -
    - ) + render: value =>
    {formatMetric(value, LARGE_FLOAT, '/s')}
    , }, { name: i18n.translate('xpack.monitoring.elasticsearch.indices.searchRateTitle', { @@ -98,10 +86,8 @@ const columns = [ field: 'search_rate', sortable: true, render: value => ( -
    - {formatMetric(value, LARGE_FLOAT, '/s')} -
    - ) +
    {formatMetric(value, LARGE_FLOAT, '/s')}
    + ), }, { name: i18n.translate('xpack.monitoring.elasticsearch.indices.unassignedShardsTitle', { @@ -109,12 +95,8 @@ const columns = [ }), field: 'unassigned_shards', sortable: true, - render: value => ( -
    - {formatMetric(value, '0')} -
    - ) - } + render: value =>
    {formatMetric(value, '0')}
    , + }, ]; const getNoDataMessage = () => { @@ -154,16 +136,16 @@ export const ElasticsearchIndices = ({ - )} + } checked={showSystemIndices} onChange={e => toggleShowSystemIndices(e.target.checked)} /> - + diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 72a74964fd35e..b06cbb44503d1 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -12,6 +12,7 @@ import { EuiMonitoringSSPTable } from '../../table'; import { MetricCell, OfflineCell } from './cells'; import { SetupModeBadge } from '../../setup_mode/badge'; import { + EuiIcon, EuiLink, EuiToolTip, EuiSpacer, @@ -21,20 +22,23 @@ import { EuiPanel, EuiCallOut, EuiButton, - EuiText + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; import { ListingCallOut } from '../../setup_mode/listing_callout'; -const getSortHandler = (type) => (item) => _.get(item, [type, 'summary', 'lastVal']); +const getSortHandler = type => item => _.get(item, [type, 'summary', 'lastVal']); const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { const cols = []; - const cpuUsageColumnTitle = i18n.translate('xpack.monitoring.elasticsearch.nodes.cpuUsageColumnTitle', { - defaultMessage: 'CPU Usage', - }); + const cpuUsageColumnTitle = i18n.translate( + 'xpack.monitoring.elasticsearch.nodes.cpuUsageColumnTitle', + { + defaultMessage: 'CPU Usage', + } + ); cols.push({ name: i18n.translate('xpack.monitoring.elasticsearch.nodes.nameColumnTitle', { @@ -59,7 +63,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { const status = list[node.resolver] || {}; const instance = { uuid: node.resolver, - name: node.name + name: node.name, }; setupModeStatus = ( @@ -82,25 +86,18 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => {
    - - + + {node.nodeTypeClass && }   - - {nameLink} - + {nameLink}
    -
    - {extractIp(node.transport_address)} -
    +
    {extractIp(node.transport_address)}
    {setupModeStatus}
    ); - } + }, }); cols.push({ @@ -110,21 +107,19 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { field: 'isOnline', sortable: true, render: value => { - const status = value ? i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumn.onlineLabel', { - defaultMessage: 'Online', - }) : i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumn.offlineLabel', { - defaultMessage: 'Offline', - }); + const status = value + ? i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumn.onlineLabel', { + defaultMessage: 'Online', + }) + : i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumn.offlineLabel', { + defaultMessage: 'Offline', + }); return (
    - {' '} - {status} + {status}
    ); - } + }, }); cols.push({ @@ -138,8 +133,10 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => {
    {value}
    - ) : ; - } + ) : ( + + ); + }, }); if (showCgroupMetricsElasticsearch) { @@ -154,7 +151,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { isPercent={true} data-test-subj="cpuQuota" /> - ) + ), }); cols.push({ @@ -170,7 +167,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { isPercent={false} data-test-subj="cpuThrottled" /> - ) + ), }); } else { cols.push({ @@ -184,7 +181,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { isPercent={true} data-test-subj="cpuUsage" /> - ) + ), }); cols.push({ @@ -200,7 +197,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { isPercent={false} data-test-subj="loadAverage" /> - ) + ), }); } @@ -208,8 +205,8 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { name: i18n.translate('xpack.monitoring.elasticsearch.nodes.jvmMemoryColumnTitle', { defaultMessage: '{javaVirtualMachine} Heap', values: { - javaVirtualMachine: 'JVM' - } + javaVirtualMachine: 'JVM', + }, }), field: 'node_jvm_mem_percent', sortable: getSortHandler('node_jvm_mem_percent'), @@ -220,7 +217,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { isPercent={true} data-test-subj="jvmMemory" /> - ) + ), }); cols.push({ @@ -236,7 +233,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { isPercent={false} data-test-subj="diskFreeSpace" /> - ) + ), }); return cols; @@ -252,18 +249,22 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear // We want to create a seamless experience for the user by merging in the setup data // and the node data from monitoring indices in the likely scenario where some nodes // are using MB collection and some are using no collection - const nodesByUuid = nodes.reduce((byUuid, node) => ({ - ...byUuid, - [node.id || node.resolver]: node - }), {}); + const nodesByUuid = nodes.reduce( + (byUuid, node) => ({ + ...byUuid, + [node.id || node.resolver]: node, + }), + {} + ); - nodes.push(...Object.entries(setupMode.data.byUuid) - .reduce((nodes, [nodeUuid, instance]) => { + nodes.push( + ...Object.entries(setupMode.data.byUuid).reduce((nodes, [nodeUuid, instance]) => { if (!nodesByUuid[nodeUuid] && instance.node) { nodes.push(instance.node); } return nodes; - }, [])); + }, []) + ); } let setupModeCallout = null; @@ -276,64 +277,81 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear customRenderer={() => { const customRenderResponse = { shouldRender: false, - componentToRender: null + componentToRender: null, }; const isNetNewUser = setupMode.data.totalUniqueInstanceCount === 0; - const hasNoInstances = setupMode.data.totalUniqueInternallyCollectedCount === 0 - && setupMode.data.totalUniqueFullyMigratedCount === 0 - && setupMode.data.totalUniquePartiallyMigratedCount === 0; + const hasNoInstances = + setupMode.data.totalUniqueInternallyCollectedCount === 0 && + setupMode.data.totalUniqueFullyMigratedCount === 0 && + setupMode.data.totalUniquePartiallyMigratedCount === 0; if (isNetNewUser || hasNoInstances) { customRenderResponse.shouldRender = true; customRenderResponse.componentToRender = ( 0 ? 'danger' : 'warning'} iconType="flag" >

    - {i18n.translate('xpack.monitoring.elasticsearch.nodes.metricbeatMigration.detectedNodeDescription', { - defaultMessage: `The following nodes are not monitored. Click 'Monitor with Metricbeat' below to start monitoring.`, - })} + {i18n.translate( + 'xpack.monitoring.elasticsearch.nodes.metricbeatMigration.detectedNodeDescription', + { + defaultMessage: `The following nodes are not monitored. Click 'Monitor with Metricbeat' below to start monitoring.`, + } + )}

    - +
    ); - } - else if (setupMode.data.totalUniquePartiallyMigratedCount === setupMode.data.totalUniqueInstanceCount) { - const finishMigrationAction = _.get(setupMode.meta, 'liveClusterUuid') === clusterUuid - ? setupMode.shortcutToFinishMigration - : setupMode.openFlyout; + } else if ( + setupMode.data.totalUniquePartiallyMigratedCount === + setupMode.data.totalUniqueInstanceCount + ) { + const finishMigrationAction = + _.get(setupMode.meta, 'liveClusterUuid') === clusterUuid + ? setupMode.shortcutToFinishMigration + : setupMode.openFlyout; customRenderResponse.shouldRender = true; customRenderResponse.componentToRender = (

    - {i18n.translate('xpack.monitoring.elasticsearch.nodes.metricbeatMigration.disableInternalCollectionDescription', { - defaultMessage: `Disable self monitoring to finish the migration.` - })} + {i18n.translate( + 'xpack.monitoring.elasticsearch.nodes.metricbeatMigration.disableInternalCollectionDescription', + { + defaultMessage: `Disable self monitoring to finish the migration.`, + } + )}

    {i18n.translate( - 'xpack.monitoring.elasticsearch.nodes.metricbeatMigration.disableInternalCollectionMigrationButtonLabel', { - defaultMessage: 'Disable self monitoring' + 'xpack.monitoring.elasticsearch.nodes.metricbeatMigration.disableInternalCollectionMigrationButtonLabel', + { + defaultMessage: 'Disable self monitoring', } )}
    - +
    ); } @@ -375,9 +393,12 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear search={{ box: { incremental: true, - placeholder: i18n.translate('xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder', { - defaultMessage: 'Filter Nodes…' - }), + placeholder: i18n.translate( + 'xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder', + { + defaultMessage: 'Filter Nodes…', + } + ), }, }} onTableChange={onTableChange} diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss index 690b1b81a0d03..50e92d572908c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss @@ -1,9 +1,3 @@ -// SASSTODO: Generic selector -monitoring-shard-allocation { - display: block; - border-top: $euiSizeS solid $euiColorLightestShade; -} - .monClusterTitle { font-size: $euiFontSizeL; margin: 0; @@ -11,55 +5,48 @@ monitoring-shard-allocation { // SASSTODO: This needs a full rewrite / redesign .monCluster { - cluster-view { - display: block; - } - .parent { - padding-top: 14px; - border-left: 3px solid $euiColorSuccess !important; - &.red { - border-left: 3px solid $euiColorDanger !important; - } - &.yellow { - border-left: 3px solid $euiColorWarning !important; - } - } - td.unassigned { + .monUnassigned { vertical-align: middle; width: 150px; } - .child { + .monUnassigned__children, + .monAssigned__children { + padding-top: $euiSizeL; + } + + .monChild { float: left; align-self: center; - + background-color: $euiColorLightestShade; + margin: $euiSizeS; + border: 1px solid $euiColorMediumShade; + border-radius: $euiSizeXS; + padding: $euiSizeXS/2 0; + // SASS-TODO: Rename this class following Eui conventions &.index { border-left: $euiSizeXS solid $euiColorSuccess; - &.red { + + &.monChild--danger { border-left: $euiSizeXS solid $euiColorDanger; } - &.yellow { + + &.monChild--warning { border-left: $euiSizeXS solid $euiColorWarning; } } - background-color: $euiColorDarkShade; - margin: 5px; - .title { - padding: 5px 7px; - display: inline-block; + + .monChild__title { + padding: $euiSizeXS $euiSizeS; text-align: center; - font-size: 12px; - font: 10px sans-serif; + font-size: $euiFontSizeXS; color: $euiColorGhost; - a { - color: $euiColorGhost; - text-decoration: none; - } - i { - margin-left: 5px; - } + display: flex; + flex-direction: row; + align-items: center; } - &.unassigned { + + &.monClusterUnassigned { .title { display: none; } @@ -73,30 +60,12 @@ monitoring-shard-allocation { td:first-child { width: 200px; } - + // SASS-TODO: Rename this class following Eui conventions .shard { align-self: center; - padding: 5px 7px; - font: 10px sans-serif; - border-left: 1px solid $euiColorEmptyShade; + padding: $euiSizeXS $euiSizeS; + font-size: $euiFontSizeXS; position: relative; display: inline-block; } - - .legend { - font-size: 12px; - background-color: $euiColorEmptyShade; - .title { - margin-left: 5px; - font-weight: bold; - } - color: $euiColorDarkestShade; - padding: 5px; - span.shard { - float: none; - display: inline-block; - margin: 0 5px 0 10px; - padding: 0 4px; - } - } } diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js index ec1b36837af92..012bc81135e34 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js @@ -4,39 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ - - import { get, sortBy } from 'lodash'; import React from 'react'; import { Shard } from './shard'; import { calculateClass } from '../lib/calculate_class'; import { generateQueryAndLink } from '../lib/generate_query_and_link'; -import { - EuiKeyboardAccessible, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiKeyboardAccessible } from '@elastic/eui'; function sortByName(item) { if (item.type === 'node') { - return [ !item.master, item.name]; + return [!item.master, item.name]; } - return [ item.name ]; + return [item.name]; } export class Assigned extends React.Component { - createShard = (shard) => { + createShard = shard => { const type = shard.primary ? 'primary' : 'replica'; const key = `${shard.index}.${shard.node}.${type}.${shard.state}.${shard.shard}`; - return ( - - ); + return ; }; - createChild = (data) => { + createChild = data => { const key = data.id; - const initialClasses = ['child']; + const initialClasses = ['monChild']; const shardStats = get(this.props.shardStats.indices, key); if (shardStats) { - initialClasses.push(shardStats.status); + switch (shardStats.status) { + case 'red': + initialClasses.push('monChild--danger'); + break; + case 'yellow': + initialClasses.push('monChild--warning'); + break; + } } const changeUrl = () => { @@ -52,28 +53,39 @@ export class Assigned extends React.Component { ); - const master = (data.node_type === 'master') ? : null; + const master = + data.node_type === 'master' ? : null; const shards = sortBy(data.children, 'shard').map(this.createShard); return ( -
    -
    {name}{master}
    - {shards} -
    + + + + {name} + {master} + + + + {shards} + + + ); }; render() { const data = sortBy(this.props.data, sortByName).map(this.createChild); return ( - -
    + + {data} -
    + ); } diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js index e350e3b037712..728165386cd18 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js @@ -4,30 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ - - import _ from 'lodash'; import React from 'react'; import { Shard } from './shard'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup } from '@elastic/eui'; export class Unassigned extends React.Component { - static displayName = i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.unassignedDisplayName', { - defaultMessage: 'Unassigned', - }); + static displayName = i18n.translate( + 'xpack.monitoring.elasticsearch.shardAllocation.unassignedDisplayName', + { + defaultMessage: 'Unassigned', + } + ); - createShard = (shard) => { + createShard = shard => { const type = shard.primary ? 'primary' : 'replica'; const additionId = shard.state === 'UNASSIGNED' ? Math.random() : ''; - const key = shard.index + '.' + shard.node + '.' + type + '.' + shard.state + '.' + shard.shard + additionId; - return (); + const key = + shard.index + + '.' + + shard.node + + '.' + + type + + '.' + + shard.state + + '.' + + shard.shard + + additionId; + return ; }; render() { const shards = _.sortBy(this.props.shards, 'shard').map(this.createShard); return ( - -
    {shards}
    + + + {shards} + ); } diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js index 50ab2653ced37..5e93e698a33a9 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js @@ -66,7 +66,7 @@ export const ShardAllocation = ({

    - + { types.map(type => ( diff --git a/x-pack/legacy/plugins/monitoring/public/components/status_icon/index.js b/x-pack/legacy/plugins/monitoring/public/components/status_icon/index.js index a054f83704176..a31823ef2e773 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/status_icon/index.js +++ b/x-pack/legacy/plugins/monitoring/public/components/status_icon/index.js @@ -5,25 +5,18 @@ */ import React from 'react'; +import { EuiIcon } from '@elastic/eui'; export function StatusIcon({ type, label }) { const typeToIconMap = { - [StatusIcon.TYPES.RED]: 'health-red.svg', - [StatusIcon.TYPES.YELLOW]: 'health-yellow.svg', - [StatusIcon.TYPES.GREEN]: 'health-green.svg', - [StatusIcon.TYPES.GRAY]: 'health-gray.svg', + [StatusIcon.TYPES.RED]: 'danger', + [StatusIcon.TYPES.YELLOW]: 'warning', + [StatusIcon.TYPES.GREEN]: 'success', + [StatusIcon.TYPES.GRAY]: 'subdued', }; const icon = typeToIconMap[type]; - return ( - - {label} - - ); + return ; } StatusIcon.TYPES = { diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap index 0842406774f73..a3d321f9e39b6 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap @@ -28,15 +28,16 @@ exports[`Summary Status Component should allow label to be optional 1`] = ` class="euiTitle euiTitle--xsmall euiStat__title" > Status: - - Status: yellow - +  Yellow

    Status: - - Status: green - +  Green

    { it('should handle incomplete shardStats data', () => { const clusterState = { nodes: { - fooNode: {} - } + fooNode: {}, + }, }; const shardStats = { nodes: { - fooNode: {} - } + fooNode: {}, + }, }; const resolver = 'fooNode'; @@ -62,7 +62,7 @@ describe('Elasticsearch Node Summary get_node_summary handleResponse', () => { totalSpace: undefined, usedHeap: undefined, nodeTypeLabel: 'Node', - nodeTypeClass: 'fa-server', + nodeTypeClass: 'storage', node_ids: [], status: 'Online', isOnline: true, @@ -72,17 +72,17 @@ describe('Elasticsearch Node Summary get_node_summary handleResponse', () => { it('should handle incomplete shardStats data, master node', () => { const clusterState = { nodes: { - 'fooNode-Uuid': {} + 'fooNode-Uuid': {}, }, - master_node: 'fooNode-Uuid' + master_node: 'fooNode-Uuid', }; const shardStats = { nodes: { 'fooNode-Uuid': { shardCount: 22, - indexCount: 11 - } - } + indexCount: 11, + }, + }, }; const resolver = 'fooNode-Uuid'; @@ -101,28 +101,28 @@ describe('Elasticsearch Node Summary get_node_summary handleResponse', () => { node_stats: { indices: { docs: { - count: 11000 + count: 11000, }, store: { - size_in_bytes: 35000 - } + size_in_bytes: 35000, + }, }, fs: { total: { available_in_bytes: 8700, - total_in_bytes: 10000 - } + total_in_bytes: 10000, + }, }, jvm: { mem: { - heap_used_percent: 33 - } - } - } - } - } - ] - } + heap_used_percent: 33, + }, + }, + }, + }, + }, + ], + }, }; const result = handleFn(response); @@ -140,10 +140,8 @@ describe('Elasticsearch Node Summary get_node_summary handleResponse', () => { totalSpace: 10000, usedHeap: 33, nodeTypeLabel: 'Master Node', - nodeTypeClass: 'fa-star', - node_ids: [ - 'fooNode-Uuid' - ], + nodeTypeClass: 'starFilled', + node_ids: ['fooNode-Uuid'], status: 'Online', isOnline: true, }); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/__tests__/get_node_type_class_label.js b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/__tests__/get_node_type_class_label.js index 4c21391a9ae62..2dc30a57db3d9 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/__tests__/get_node_type_class_label.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/__tests__/get_node_type_class_label.js @@ -11,12 +11,12 @@ describe('Node Type and Label', () => { describe('when master node', () => { it('type is indicated by boolean flag', () => { const node = { - master: true + master: true, }; const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel(node); expect(nodeType).to.be('master'); expect(nodeTypeLabel).to.be('Master Node'); - expect(nodeTypeClass).to.be('fa-star'); + expect(nodeTypeClass).to.be('starFilled'); }); it('type is indicated by string', () => { const node = {}; @@ -24,7 +24,7 @@ describe('Node Type and Label', () => { const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel(node, type); expect(nodeType).to.be('master'); expect(nodeTypeLabel).to.be('Master Node'); - expect(nodeTypeClass).to.be('fa-star'); + expect(nodeTypeClass).to.be('starFilled'); }); }); it('when type is generic node', () => { @@ -33,6 +33,6 @@ describe('Node Type and Label', () => { const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel(node, type); expect(nodeType).to.be('node'); expect(nodeTypeLabel).to.be('Node'); - expect(nodeTypeClass).to.be('fa-server'); + expect(nodeTypeClass).to.be('storage'); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap index ba72d697388c6..db74cc5e330a1 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap @@ -5,7 +5,7 @@ Array [ Object { "isOnline": false, "name": "hello01", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "resolver": "_x_V2YzPQU-a9KRRBxUxZQ", "shardCount": 6, @@ -15,7 +15,7 @@ Array [ Object { "isOnline": false, "name": "hello02", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "resolver": "DAiX7fFjS3Wii7g2HYKrOg", "shardCount": 6, @@ -32,7 +32,7 @@ Array [ Object { "isOnline": true, "name": "hello01", - "nodeTypeClass": "fa-star", + "nodeTypeClass": "starFilled", "nodeTypeLabel": "Master Node", "node_cgroup_quota": Object { "metric": Object { @@ -160,7 +160,7 @@ Array [ Object { "isOnline": true, "name": "hello02", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "node_cgroup_quota": undefined, "node_cgroup_throttled": Object { @@ -274,7 +274,7 @@ Array [ Object { "isOnline": true, "name": "hello01", - "nodeTypeClass": "fa-star", + "nodeTypeClass": "starFilled", "nodeTypeLabel": "Master Node", "node_cgroup_quota": null, "node_cgroup_throttled": null, @@ -290,7 +290,7 @@ Array [ Object { "isOnline": true, "name": "hello02", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "node_cgroup_quota": null, "node_cgroup_throttled": null, @@ -311,7 +311,7 @@ Array [ Object { "isOnline": true, "name": "hello01", - "nodeTypeClass": "fa-star", + "nodeTypeClass": "starFilled", "nodeTypeLabel": "Master Node", "node_cgroup_quota": Object { "metric": Object { @@ -439,7 +439,7 @@ Array [ Object { "isOnline": true, "name": "hello02", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "node_cgroup_quota": undefined, "node_cgroup_throttled": Object { diff --git a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/map_nodes_info.test.js.snap b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/map_nodes_info.test.js.snap index 9f75dd1f1ee0f..7eb22b0063745 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/map_nodes_info.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/map_nodes_info.test.js.snap @@ -5,7 +5,7 @@ Object { "ENVgDIKRSdCVJo-YqY4kUQ": Object { "isOnline": true, "name": "node01", - "nodeTypeClass": "fa-star", + "nodeTypeClass": "starFilled", "nodeTypeLabel": "Master Node", "shardCount": 57, "transport_address": "127.0.0.1:9300", @@ -14,7 +14,7 @@ Object { "t9J9jvHpQ2yDw9c1LJ0tHA": Object { "isOnline": false, "name": "node02", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "shardCount": 0, "transport_address": "127.0.0.1:9301", diff --git a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/lookups.js b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/lookups.js index f8d97acf792c3..23b4021ee7c0c 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/lookups.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/elasticsearch/nodes/lookups.js @@ -12,25 +12,31 @@ import { i18n } from '@kbn/i18n'; export const nodeTypeClass = { - invalid: 'fa-exclamation-triangle', - node: 'fa-server', - master: 'fa-star', - master_only: 'fa-star-o', - data: 'fa-database', - client: 'fa-binoculars' + invalid: 'alert', + node: 'storage', + master: 'starFilled', + master_only: 'starEmpty', + data: 'database', + client: 'glasses', }; export const nodeTypeLabel = { invalid: i18n.translate('xpack.monitoring.es.nodeType.invalidNodeLabel', { - defaultMessage: 'Invalid Node' }), + defaultMessage: 'Invalid Node', + }), node: i18n.translate('xpack.monitoring.es.nodeType.nodeLabel', { - defaultMessage: 'Node' }), + defaultMessage: 'Node', + }), master: i18n.translate('xpack.monitoring.es.nodeType.masterNodeLabel', { - defaultMessage: 'Master Node' }), + defaultMessage: 'Master Node', + }), master_only: i18n.translate('xpack.monitoring.es.nodeType.masterOnlyNodeLabel', { - defaultMessage: 'Master Only Node' }), + defaultMessage: 'Master Only Node', + }), data: i18n.translate('xpack.monitoring.es.nodeType.dataOnlyNodeLabel', { - defaultMessage: 'Data Only Node' }), + defaultMessage: 'Data Only Node', + }), client: i18n.translate('xpack.monitoring.es.nodeType.clientNodeLabel', { - defaultMessage: 'Client Node' }) + defaultMessage: 'Client Node', + }), }; diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/node_detail_advanced.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/node_detail_advanced.json index f84d0c73bed07..2eb7d54effdfb 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/node_detail_advanced.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/node_detail_advanced.json @@ -10,7 +10,7 @@ "name": "whatever-01", "type": "master", "nodeTypeLabel": "Master Node", - "nodeTypeClass": "fa-star", + "nodeTypeClass": "starFilled", "totalShards": 38, "indexCount": 20, "documents": 24830, @@ -22,1540 +22,1594 @@ "isOnline": true }, "metrics": { - "node_jvm_mem": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.jvm.mem.heap_max_in_bytes", - "metricAgg": "max", - "label": "Max Heap", - "title": "JVM Heap", - "description": "Total heap available to Elasticsearch running in the JVM.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 709623808], - [1507235530000, 709623808], - [1507235540000, 709623808], - [1507235550000, 709623808], - [1507235560000, 709623808], - [1507235570000, 709623808], - [1507235580000, 709623808], - [1507235590000, 709623808], - [1507235600000, 709623808], - [1507235610000, 709623808], - [1507235620000, 709623808], - [1507235630000, 709623808], - [1507235640000, 709623808], - [1507235650000, 709623808], - [1507235660000, 709623808], - [1507235670000, 709623808], - [1507235680000, 709623808], - [1507235690000, 709623808], - [1507235700000, 709623808] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.jvm.mem.heap_used_in_bytes", - "metricAgg": "max", - "label": "Used Heap", - "title": "JVM Heap", - "description": "Total heap used by Elasticsearch running in the JVM.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 317052776], - [1507235530000, 344014976], - [1507235540000, 368593248], - [1507235550000, 253850400], - [1507235560000, 348095032], - [1507235570000, 182919712], - [1507235580000, 212395016], - [1507235590000, 244004144], - [1507235600000, 270412240], - [1507235610000, 245052864], - [1507235620000, 370270616], - [1507235630000, 196944168], - [1507235640000, 223491760], - [1507235650000, 253878472], - [1507235660000, 280811736], - [1507235670000, 371931976], - [1507235680000, 329874616], - [1507235690000, 363869776], - [1507235700000, 211045968] - ] - }], - "node_gc": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.jvm.gc.collectors.old.collection_count", - "metricAgg": "max", - "label": "Old", - "title": "GC Rate", - "description": "Number of old Garbage Collections.", - "units": "", - "format": "0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.jvm.gc.collectors.young.collection_count", - "metricAgg": "max", - "label": "Young", - "title": "GC Rate", - "description": "Number of young Garbage Collections.", - "units": "", - "format": "0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0.1], - [1507235560000, 0], - [1507235570000, 0.1], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0.1], - [1507235620000, 0], - [1507235630000, 0.1], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0.1], - [1507235690000, 0], - [1507235700000, 0.1] - ] - }], - "node_gc_time": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.jvm.gc.collectors.old.collection_time_in_millis", - "metricAgg": "max", - "label": "Old", - "title": "GC Duration", - "description": "Time spent performing old Garbage Collections.", - "units": "ms", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.jvm.gc.collectors.young.collection_time_in_millis", - "metricAgg": "max", - "label": "Young", - "title": "GC Duration", - "description": "Time spent performing young Garbage Collections.", - "units": "ms", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 1.1], - [1507235560000, 0], - [1507235570000, 1.2], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 1], - [1507235620000, 0], - [1507235630000, 1.1], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 2.9], - [1507235690000, 0], - [1507235700000, 2.1] - ] - }], - "node_index_1": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.memory_in_bytes", - "metricAgg": "max", - "label": "Lucene Total", - "title": "Index Memory - Lucene 1", - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 4797457], - [1507235530000, 4797457], - [1507235540000, 4797457], - [1507235550000, 4797457], - [1507235560000, 4823580], - [1507235570000, 4823580], - [1507235580000, 4823580], - [1507235590000, 4823580], - [1507235600000, 4823580], - [1507235610000, 4838368], - [1507235620000, 4741420], - [1507235630000, 4741420], - [1507235640000, 4741420], - [1507235650000, 4741420], - [1507235660000, 4741420], - [1507235670000, 4757998], - [1507235680000, 4787542], - [1507235690000, 4787542], - [1507235700000, 4787542] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.stored_fields_memory_in_bytes", - "metricAgg": "max", - "label": "Stored Fields", - "title": "Index Memory", - "description": "Heap memory used by Stored Fields (e.g., _source). This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 56792], - [1507235530000, 56792], - [1507235540000, 56792], - [1507235550000, 56792], - [1507235560000, 57728], - [1507235570000, 57728], - [1507235580000, 57728], - [1507235590000, 57728], - [1507235600000, 57728], - [1507235610000, 58352], - [1507235620000, 56192], - [1507235630000, 56192], - [1507235640000, 56192], - [1507235650000, 56192], - [1507235660000, 56192], - [1507235670000, 56816], - [1507235680000, 57440], - [1507235690000, 57440], - [1507235700000, 57440] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.doc_values_memory_in_bytes", - "metricAgg": "max", - "label": "Doc Values", - "title": "Index Memory", - "description": "Heap memory used by Doc Values. This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 516824], - [1507235530000, 516824], - [1507235540000, 516824], - [1507235550000, 516824], - [1507235560000, 517292], - [1507235570000, 517292], - [1507235580000, 517292], - [1507235590000, 517292], - [1507235600000, 517292], - [1507235610000, 517612], - [1507235620000, 514808], - [1507235630000, 514808], - [1507235640000, 514808], - [1507235650000, 514808], - [1507235660000, 514808], - [1507235670000, 515312], - [1507235680000, 516008], - [1507235690000, 516008], - [1507235700000, 516008] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.norms_memory_in_bytes", - "metricAgg": "max", - "label": "Norms", - "title": "Index Memory", - "description": "Heap memory used by Norms (normalization factors for query-time, text scoring). This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 447232], - [1507235530000, 447232], - [1507235540000, 447232], - [1507235550000, 447232], - [1507235560000, 449600], - [1507235570000, 449600], - [1507235580000, 449600], - [1507235590000, 449600], - [1507235600000, 449600], - [1507235610000, 450880], - [1507235620000, 442304], - [1507235630000, 442304], - [1507235640000, 442304], - [1507235650000, 442304], - [1507235660000, 442304], - [1507235670000, 443840], - [1507235680000, 446400], - [1507235690000, 446400], - [1507235700000, 446400] - ] - }], - "node_index_2": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.memory_in_bytes", - "metricAgg": "max", - "label": "Lucene Total", - "title": "Index Memory - Lucene 2", - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 4797457], - [1507235530000, 4797457], - [1507235540000, 4797457], - [1507235550000, 4797457], - [1507235560000, 4823580], - [1507235570000, 4823580], - [1507235580000, 4823580], - [1507235590000, 4823580], - [1507235600000, 4823580], - [1507235610000, 4838368], - [1507235620000, 4741420], - [1507235630000, 4741420], - [1507235640000, 4741420], - [1507235650000, 4741420], - [1507235660000, 4741420], - [1507235670000, 4757998], - [1507235680000, 4787542], - [1507235690000, 4787542], - [1507235700000, 4787542] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.terms_memory_in_bytes", - "metricAgg": "max", - "label": "Terms", - "title": "Index Memory", - "description": "Heap memory used by Terms (e.g., text). This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 3764438], - [1507235530000, 3764438], - [1507235540000, 3764438], - [1507235550000, 3764438], - [1507235560000, 3786762], - [1507235570000, 3786762], - [1507235580000, 3786762], - [1507235590000, 3786762], - [1507235600000, 3786762], - [1507235610000, 3799306], - [1507235620000, 3715996], - [1507235630000, 3715996], - [1507235640000, 3715996], - [1507235650000, 3715996], - [1507235660000, 3715996], - [1507235670000, 3729890], - [1507235680000, 3755528], - [1507235690000, 3755528], - [1507235700000, 3755528] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.points_memory_in_bytes", - "metricAgg": "max", - "label": "Points", - "title": "Index Memory", - "description": "Heap memory used by Points (e.g., numbers, IPs, and geo data). This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 12171], - [1507235530000, 12171], - [1507235540000, 12171], - [1507235550000, 12171], - [1507235560000, 12198], - [1507235570000, 12198], - [1507235580000, 12198], - [1507235590000, 12198], - [1507235600000, 12198], - [1507235610000, 12218], - [1507235620000, 12120], - [1507235630000, 12120], - [1507235640000, 12120], - [1507235650000, 12120], - [1507235660000, 12120], - [1507235670000, 12140], - [1507235680000, 12166], - [1507235690000, 12166], - [1507235700000, 12166] - ] - }], - "node_index_3": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.memory_in_bytes", - "metricAgg": "max", - "label": "Lucene Total", - "title": "Index Memory - Lucene 3", - "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 4797457], - [1507235530000, 4797457], - [1507235540000, 4797457], - [1507235550000, 4797457], - [1507235560000, 4823580], - [1507235570000, 4823580], - [1507235580000, 4823580], - [1507235590000, 4823580], - [1507235600000, 4823580], - [1507235610000, 4838368], - [1507235620000, 4741420], - [1507235630000, 4741420], - [1507235640000, 4741420], - [1507235650000, 4741420], - [1507235660000, 4741420], - [1507235670000, 4757998], - [1507235680000, 4787542], - [1507235690000, 4787542], - [1507235700000, 4787542] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.fixed_bit_set_memory_in_bytes", - "metricAgg": "max", - "label": "Fixed Bitsets", - "title": "Index Memory", - "description": "Heap memory used by Fixed Bit Sets (e.g., deeply nested documents). This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 4024], - [1507235530000, 4024], - [1507235540000, 4024], - [1507235550000, 4024], - [1507235560000, 4120], - [1507235570000, 4120], - [1507235580000, 4120], - [1507235590000, 4120], - [1507235600000, 4120], - [1507235610000, 4168], - [1507235620000, 3832], - [1507235630000, 3832], - [1507235640000, 3832], - [1507235650000, 3832], - [1507235660000, 3832], - [1507235670000, 3880], - [1507235680000, 3976], - [1507235690000, 3976], - [1507235700000, 3976] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.term_vectors_memory_in_bytes", - "metricAgg": "max", - "label": "Term Vectors", - "title": "Index Memory", - "description": "Heap memory used by Term Vectors. This is a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 0], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.version_map_memory_in_bytes", - "metricAgg": "max", - "label": "Version Map", - "title": "Index Memory", - "description": "Heap memory used by Versioning (e.g., updates and deletes). This is NOT a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 5551], - [1507235530000, 5551], - [1507235540000, 5551], - [1507235550000, 6594], - [1507235560000, 6662], - [1507235570000, 6662], - [1507235580000, 6662], - [1507235590000, 6662], - [1507235600000, 6662], - [1507235610000, 7531], - [1507235620000, 7837], - [1507235630000, 7837], - [1507235640000, 7837], - [1507235650000, 7837], - [1507235660000, 7837], - [1507235670000, 9974], - [1507235680000, 9716], - [1507235690000, 9716], - [1507235700000, 9716] - ] - }], - "node_index_4": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.query_cache.memory_size_in_bytes", - "metricAgg": "max", - "label": "Query Cache", - "title": "Index Memory - Elasticsearch", - "description": "Heap memory used by Query Cache (e.g., cached filters). This is for the same shards, but not a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 0], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.request_cache.memory_size_in_bytes", - "metricAgg": "max", - "label": "Request Cache", - "title": "Index Memory", - "description": "Heap memory used by Request Cache (e.g., instant aggregations). This is for the same shards, but not a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 2921], - [1507235530000, 2921], - [1507235540000, 2921], - [1507235550000, 2921], - [1507235560000, 2921], - [1507235570000, 2921], - [1507235580000, 2921], - [1507235590000, 2921], - [1507235600000, 2921], - [1507235610000, 2921], - [1507235620000, 2921], - [1507235630000, 2921], - [1507235640000, 2921], - [1507235650000, 2921], - [1507235660000, 2921], - [1507235670000, 2921], - [1507235680000, 2921], - [1507235690000, 2921], - [1507235700000, 2921] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.fielddata.memory_size_in_bytes", - "metricAgg": "max", - "label": "Fielddata", - "title": "Index Memory", - "description": "Heap memory used by Fielddata (e.g., global ordinals or explicitly enabled fielddata on text fields). This is for the same shards, but not a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 0], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.segments.index_writer_memory_in_bytes", - "metricAgg": "max", - "label": "Index Writer", - "title": "Index Memory", - "description": "Heap memory used by the Index Writer. This is NOT a part of Lucene Total.", - "units": "B", - "format": "0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 153549], - [1507235530000, 153549], - [1507235540000, 153549], - [1507235550000, 849833], - [1507235560000, 156505], - [1507235570000, 156505], - [1507235580000, 156505], - [1507235590000, 156505], - [1507235600000, 156505], - [1507235610000, 3140275], - [1507235620000, 159637], - [1507235630000, 159637], - [1507235640000, 159637], - [1507235650000, 159637], - [1507235660000, 159637], - [1507235670000, 3737997], - [1507235680000, 164351], - [1507235690000, 164351], - [1507235700000, 164351] - ] - }], - "node_request_total": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.search.query_total", - "metricAgg": "max", - "label": "Search Total", - "title": "Request Rate", - "description": "Amount of search operations (per shard).", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0.3], - [1507235540000, 0.3], - [1507235550000, 0.3], - [1507235560000, 0.3], - [1507235570000, 0.3], - [1507235580000, 0.3], - [1507235590000, 0.4], - [1507235600000, 0.3], - [1507235610000, 0.5], - [1507235620000, 0.3], - [1507235630000, 0.3], - [1507235640000, 0.2], - [1507235650000, 0.3], - [1507235660000, 0.3], - [1507235670000, 0.5], - [1507235680000, 0.5], - [1507235690000, 0.1], - [1507235700000, 0.4] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.indexing.index_total", - "metricAgg": "max", - "label": "Indexing Total", - "title": "Request Rate", - "description": "Amount of indexing operations.", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0.9], - [1507235560000, 0.6], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0.9], - [1507235620000, 0.6], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 1.8], - [1507235680000, 0.8], - [1507235690000, 0], - [1507235700000, 0] - ] - }], - "node_index_time": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.indexing.index_time_in_millis", - "metricAgg": "max", - "label": "Index Time", - "title": "Indexing Time", - "description": "Amount of time spent on indexing operations.", - "units": "ms", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0.8], - [1507235560000, 0.7], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 1.2], - [1507235620000, 0.7], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 4.2], - [1507235680000, 2.3], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.indexing.throttle_time_in_millis", - "metricAgg": "max", - "label": "Index Throttling Time", - "title": "Indexing Time", - "description": "Amount of time spent with index throttling, which indicates slow disks on a node.", - "units": "ms", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }], - "node_index_threads": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.thread_pool.write.queue", - "metricAgg": "max", - "label": "Write Queue", - "title": "Indexing Threads", - "description": "Number of index, bulk, and write operations in the queue. The bulk threadpool was renamed to write in 6.3, and the index threadpool is deprecated.", - "units": "", - "format": "0.[00]", - "hasCalculation": true, - "isDerivative": false - }, - "data": [ - [1507235520000, 0], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.thread_pool.write.rejected", - "metricAgg": "max", - "label": "Write Rejections", - "title": "Indexing Threads", - "description": "Number of index, bulk, and write operations that have been rejected, which occurs when the queue is full. The bulk threadpool was renamed to write in 6.3, and the index threadpool is deprecated.", - "units": "", - "format": "0.[00]", - "hasCalculation": true, - "isDerivative": false - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }], - "node_read_threads": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.thread_pool.search.queue", - "metricAgg": "max", - "label": "Search Queue", - "title": "Read Threads", - "description": "Number of search operations in the queue (e.g., shard level searches).", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0.2], - [1507235680000, null], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.thread_pool.search.rejected", - "metricAgg": "max", - "label": "Search Rejections", - "title": "Read Threads", - "description": "Number of search operations that have been rejected, which occurs when the queue is full.", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.thread_pool.get.queue", - "metricAgg": "max", - "label": "GET Queue", - "title": "Read Threads", - "description": "Number of GET operations in the queue.", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.thread_pool.get.rejected", - "metricAgg": "max", - "label": "GET Rejections", - "title": "Read Threads", - "description": "Number of GET operations that have been rejected, which occurs when the queue is full.", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0], - [1507235560000, 0], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }], - "node_cpu_utilization": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.process.cpu.percent", - "metricAgg": "max", - "label": "CPU Utilization", - "description": "Percentage of CPU usage for the Elasticsearch process.", - "units": "%", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1507235520000, 1], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 1], - [1507235560000, 2], - [1507235570000, 0], - [1507235580000, 2], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 3], - [1507235620000, 2], - [1507235630000, 2], - [1507235640000, 0], - [1507235650000, 1], - [1507235660000, 0], - [1507235670000, 2], - [1507235680000, 2], - [1507235690000, 1], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.process.cpu.percent", - "metricAgg": "max", - "label": "Cgroup CPU Utilization", - "title": "CPU Utilization", - "description": "CPU Usage time compared to the CPU quota shown in percentage. If CPU quotas are not set, then no data will be shown.", - "units": "%", - "format": "0,0.[00]", - "hasCalculation": true, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, null], - [1507235540000, null], - [1507235550000, null], - [1507235560000, null], - [1507235570000, null], - [1507235580000, null], - [1507235590000, null], - [1507235600000, null], - [1507235610000, null], - [1507235620000, null], - [1507235630000, null], - [1507235640000, null], - [1507235650000, null], - [1507235660000, null], - [1507235670000, null], - [1507235680000, null], - [1507235690000, null], - [1507235700000, null] - ] - }], - "node_cgroup_cpu": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.os.cgroup.cpuacct.usage_nanos", - "metricAgg": "max", - "label": "Cgroup Usage", - "title": "Cgroup CPU Performance", - "description": "The usage, reported in nanoseconds, of the cgroup. Compare this with the throttling to discover issues.", - "units": "ns", - "format": "0,0.[0]a", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, null], - [1507235540000, null], - [1507235550000, null], - [1507235560000, null], - [1507235570000, null], - [1507235580000, null], - [1507235590000, null], - [1507235600000, null], - [1507235610000, null], - [1507235620000, null], - [1507235630000, null], - [1507235640000, null], - [1507235650000, null], - [1507235660000, null], - [1507235670000, null], - [1507235680000, null], - [1507235690000, null], - [1507235700000, null] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.os.cgroup.cpu.stat.time_throttled_nanos", - "metricAgg": "max", - "label": "Cgroup Throttling", - "title": "Cgroup CPU Performance", - "description": "The amount of throttled time, reported in nanoseconds, of the cgroup.", - "units": "ns", - "format": "0,0.[0]a", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, null], - [1507235540000, null], - [1507235550000, null], - [1507235560000, null], - [1507235570000, null], - [1507235580000, null], - [1507235590000, null], - [1507235600000, null], - [1507235610000, null], - [1507235620000, null], - [1507235630000, null], - [1507235640000, null], - [1507235650000, null], - [1507235660000, null], - [1507235670000, null], - [1507235680000, null], - [1507235690000, null], - [1507235700000, null] - ] - }], - "node_cgroup_stats": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods", - "metricAgg": "max", - "label": "Cgroup Elapsed Periods", - "title": "Cgroup CFS Stats", - "description": "The number of sampling periods from the Completely Fair Scheduler (CFS). Compare against the number of times throttled.", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, null], - [1507235540000, null], - [1507235550000, null], - [1507235560000, null], - [1507235570000, null], - [1507235580000, null], - [1507235590000, null], - [1507235600000, null], - [1507235610000, null], - [1507235620000, null], - [1507235630000, null], - [1507235640000, null], - [1507235650000, null], - [1507235660000, null], - [1507235670000, null], - [1507235680000, null], - [1507235690000, null], - [1507235700000, null] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.os.cgroup.cpu.stat.number_of_times_throttled", - "metricAgg": "max", - "label": "Cgroup Throttled Count", - "title": "Cgroup CFS Stats", - "description": "The number of times that the CPU was throttled by the cgroup.", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1507235520000, null], - [1507235530000, null], - [1507235540000, null], - [1507235550000, null], - [1507235560000, null], - [1507235570000, null], - [1507235580000, null], - [1507235590000, null], - [1507235600000, null], - [1507235610000, null], - [1507235620000, null], - [1507235630000, null], - [1507235640000, null], - [1507235650000, null], - [1507235660000, null], - [1507235670000, null], - [1507235680000, null], - [1507235690000, null], - [1507235700000, null] - ] - }], - "node_latency": [{ - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.search.query_total", - "metricAgg": "sum", - "label": "Search", - "title": "Latency", - "description": "Average latency for searching, which is time it takes to execute searches divided by number of searches submitted. This considers primary and replica shards.", - "units": "ms", - "format": "0,0.[00]", - "hasCalculation": true, - "isDerivative": false - }, - "data": [ - [1507235520000, null], - [1507235530000, 0.33333333333333337], - [1507235540000, 0], - [1507235550000, 0.33333333333333337], - [1507235560000, 0], - [1507235570000, 0.33333333333333337], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0.33333333333333337], - [1507235610000, 0], - [1507235620000, 0], - [1507235630000, 0.33333333333333337], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 0.2], - [1507235680000, 0], - [1507235690000, 0], - [1507235700000, 0] - ] - }, { - "bucket_size": "10 seconds", - "timeRange": { - "min": 1507235508000, - "max": 1507235712000 - }, - "metric": { - "app": "elasticsearch", - "field": "node_stats.indices.indexing.index_total", - "metricAgg": "sum", - "label": "Indexing", - "title": "Latency", - "description": "Average latency for indexing documents, which is time it takes to index documents divided by number that were indexed. This considers any shard located on this node, including replicas.", - "units": "ms", - "format": "0,0.[00]", - "hasCalculation": true, - "isDerivative": false - }, - "data": [ - [1507235520000, null], - [1507235530000, 0], - [1507235540000, 0], - [1507235550000, 0.888888888888889], - [1507235560000, 1.1666666666666667], - [1507235570000, 0], - [1507235580000, 0], - [1507235590000, 0], - [1507235600000, 0], - [1507235610000, 1.3333333333333333], - [1507235620000, 1.1666666666666667], - [1507235630000, 0], - [1507235640000, 0], - [1507235650000, 0], - [1507235660000, 0], - [1507235670000, 2.3333333333333335], - [1507235680000, 2.8749999999999996], - [1507235690000, 0], - [1507235700000, 0] - ] - }] + "node_jvm_mem": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.jvm.mem.heap_max_in_bytes", + "metricAgg": "max", + "label": "Max Heap", + "title": "JVM Heap", + "description": "Total heap available to Elasticsearch running in the JVM.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 709623808], + [1507235530000, 709623808], + [1507235540000, 709623808], + [1507235550000, 709623808], + [1507235560000, 709623808], + [1507235570000, 709623808], + [1507235580000, 709623808], + [1507235590000, 709623808], + [1507235600000, 709623808], + [1507235610000, 709623808], + [1507235620000, 709623808], + [1507235630000, 709623808], + [1507235640000, 709623808], + [1507235650000, 709623808], + [1507235660000, 709623808], + [1507235670000, 709623808], + [1507235680000, 709623808], + [1507235690000, 709623808], + [1507235700000, 709623808] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.jvm.mem.heap_used_in_bytes", + "metricAgg": "max", + "label": "Used Heap", + "title": "JVM Heap", + "description": "Total heap used by Elasticsearch running in the JVM.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 317052776], + [1507235530000, 344014976], + [1507235540000, 368593248], + [1507235550000, 253850400], + [1507235560000, 348095032], + [1507235570000, 182919712], + [1507235580000, 212395016], + [1507235590000, 244004144], + [1507235600000, 270412240], + [1507235610000, 245052864], + [1507235620000, 370270616], + [1507235630000, 196944168], + [1507235640000, 223491760], + [1507235650000, 253878472], + [1507235660000, 280811736], + [1507235670000, 371931976], + [1507235680000, 329874616], + [1507235690000, 363869776], + [1507235700000, 211045968] + ] + } + ], + "node_gc": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.jvm.gc.collectors.old.collection_count", + "metricAgg": "max", + "label": "Old", + "title": "GC Rate", + "description": "Number of old Garbage Collections.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.jvm.gc.collectors.young.collection_count", + "metricAgg": "max", + "label": "Young", + "title": "GC Rate", + "description": "Number of young Garbage Collections.", + "units": "", + "format": "0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0.1], + [1507235560000, 0], + [1507235570000, 0.1], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0.1], + [1507235620000, 0], + [1507235630000, 0.1], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0.1], + [1507235690000, 0], + [1507235700000, 0.1] + ] + } + ], + "node_gc_time": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.jvm.gc.collectors.old.collection_time_in_millis", + "metricAgg": "max", + "label": "Old", + "title": "GC Duration", + "description": "Time spent performing old Garbage Collections.", + "units": "ms", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.jvm.gc.collectors.young.collection_time_in_millis", + "metricAgg": "max", + "label": "Young", + "title": "GC Duration", + "description": "Time spent performing young Garbage Collections.", + "units": "ms", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 1.1], + [1507235560000, 0], + [1507235570000, 1.2], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 1], + [1507235620000, 0], + [1507235630000, 1.1], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 2.9], + [1507235690000, 0], + [1507235700000, 2.1] + ] + } + ], + "node_index_1": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.memory_in_bytes", + "metricAgg": "max", + "label": "Lucene Total", + "title": "Index Memory - Lucene 1", + "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 4797457], + [1507235530000, 4797457], + [1507235540000, 4797457], + [1507235550000, 4797457], + [1507235560000, 4823580], + [1507235570000, 4823580], + [1507235580000, 4823580], + [1507235590000, 4823580], + [1507235600000, 4823580], + [1507235610000, 4838368], + [1507235620000, 4741420], + [1507235630000, 4741420], + [1507235640000, 4741420], + [1507235650000, 4741420], + [1507235660000, 4741420], + [1507235670000, 4757998], + [1507235680000, 4787542], + [1507235690000, 4787542], + [1507235700000, 4787542] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.stored_fields_memory_in_bytes", + "metricAgg": "max", + "label": "Stored Fields", + "title": "Index Memory", + "description": "Heap memory used by Stored Fields (e.g., _source). This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 56792], + [1507235530000, 56792], + [1507235540000, 56792], + [1507235550000, 56792], + [1507235560000, 57728], + [1507235570000, 57728], + [1507235580000, 57728], + [1507235590000, 57728], + [1507235600000, 57728], + [1507235610000, 58352], + [1507235620000, 56192], + [1507235630000, 56192], + [1507235640000, 56192], + [1507235650000, 56192], + [1507235660000, 56192], + [1507235670000, 56816], + [1507235680000, 57440], + [1507235690000, 57440], + [1507235700000, 57440] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.doc_values_memory_in_bytes", + "metricAgg": "max", + "label": "Doc Values", + "title": "Index Memory", + "description": "Heap memory used by Doc Values. This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 516824], + [1507235530000, 516824], + [1507235540000, 516824], + [1507235550000, 516824], + [1507235560000, 517292], + [1507235570000, 517292], + [1507235580000, 517292], + [1507235590000, 517292], + [1507235600000, 517292], + [1507235610000, 517612], + [1507235620000, 514808], + [1507235630000, 514808], + [1507235640000, 514808], + [1507235650000, 514808], + [1507235660000, 514808], + [1507235670000, 515312], + [1507235680000, 516008], + [1507235690000, 516008], + [1507235700000, 516008] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.norms_memory_in_bytes", + "metricAgg": "max", + "label": "Norms", + "title": "Index Memory", + "description": "Heap memory used by Norms (normalization factors for query-time, text scoring). This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 447232], + [1507235530000, 447232], + [1507235540000, 447232], + [1507235550000, 447232], + [1507235560000, 449600], + [1507235570000, 449600], + [1507235580000, 449600], + [1507235590000, 449600], + [1507235600000, 449600], + [1507235610000, 450880], + [1507235620000, 442304], + [1507235630000, 442304], + [1507235640000, 442304], + [1507235650000, 442304], + [1507235660000, 442304], + [1507235670000, 443840], + [1507235680000, 446400], + [1507235690000, 446400], + [1507235700000, 446400] + ] + } + ], + "node_index_2": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.memory_in_bytes", + "metricAgg": "max", + "label": "Lucene Total", + "title": "Index Memory - Lucene 2", + "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 4797457], + [1507235530000, 4797457], + [1507235540000, 4797457], + [1507235550000, 4797457], + [1507235560000, 4823580], + [1507235570000, 4823580], + [1507235580000, 4823580], + [1507235590000, 4823580], + [1507235600000, 4823580], + [1507235610000, 4838368], + [1507235620000, 4741420], + [1507235630000, 4741420], + [1507235640000, 4741420], + [1507235650000, 4741420], + [1507235660000, 4741420], + [1507235670000, 4757998], + [1507235680000, 4787542], + [1507235690000, 4787542], + [1507235700000, 4787542] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.terms_memory_in_bytes", + "metricAgg": "max", + "label": "Terms", + "title": "Index Memory", + "description": "Heap memory used by Terms (e.g., text). This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 3764438], + [1507235530000, 3764438], + [1507235540000, 3764438], + [1507235550000, 3764438], + [1507235560000, 3786762], + [1507235570000, 3786762], + [1507235580000, 3786762], + [1507235590000, 3786762], + [1507235600000, 3786762], + [1507235610000, 3799306], + [1507235620000, 3715996], + [1507235630000, 3715996], + [1507235640000, 3715996], + [1507235650000, 3715996], + [1507235660000, 3715996], + [1507235670000, 3729890], + [1507235680000, 3755528], + [1507235690000, 3755528], + [1507235700000, 3755528] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.points_memory_in_bytes", + "metricAgg": "max", + "label": "Points", + "title": "Index Memory", + "description": "Heap memory used by Points (e.g., numbers, IPs, and geo data). This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 12171], + [1507235530000, 12171], + [1507235540000, 12171], + [1507235550000, 12171], + [1507235560000, 12198], + [1507235570000, 12198], + [1507235580000, 12198], + [1507235590000, 12198], + [1507235600000, 12198], + [1507235610000, 12218], + [1507235620000, 12120], + [1507235630000, 12120], + [1507235640000, 12120], + [1507235650000, 12120], + [1507235660000, 12120], + [1507235670000, 12140], + [1507235680000, 12166], + [1507235690000, 12166], + [1507235700000, 12166] + ] + } + ], + "node_index_3": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.memory_in_bytes", + "metricAgg": "max", + "label": "Lucene Total", + "title": "Index Memory - Lucene 3", + "description": "Total heap memory used by Lucene for current index. This is the sum of other fields for primary and replica shards on this node.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 4797457], + [1507235530000, 4797457], + [1507235540000, 4797457], + [1507235550000, 4797457], + [1507235560000, 4823580], + [1507235570000, 4823580], + [1507235580000, 4823580], + [1507235590000, 4823580], + [1507235600000, 4823580], + [1507235610000, 4838368], + [1507235620000, 4741420], + [1507235630000, 4741420], + [1507235640000, 4741420], + [1507235650000, 4741420], + [1507235660000, 4741420], + [1507235670000, 4757998], + [1507235680000, 4787542], + [1507235690000, 4787542], + [1507235700000, 4787542] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.fixed_bit_set_memory_in_bytes", + "metricAgg": "max", + "label": "Fixed Bitsets", + "title": "Index Memory", + "description": "Heap memory used by Fixed Bit Sets (e.g., deeply nested documents). This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 4024], + [1507235530000, 4024], + [1507235540000, 4024], + [1507235550000, 4024], + [1507235560000, 4120], + [1507235570000, 4120], + [1507235580000, 4120], + [1507235590000, 4120], + [1507235600000, 4120], + [1507235610000, 4168], + [1507235620000, 3832], + [1507235630000, 3832], + [1507235640000, 3832], + [1507235650000, 3832], + [1507235660000, 3832], + [1507235670000, 3880], + [1507235680000, 3976], + [1507235690000, 3976], + [1507235700000, 3976] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.term_vectors_memory_in_bytes", + "metricAgg": "max", + "label": "Term Vectors", + "title": "Index Memory", + "description": "Heap memory used by Term Vectors. This is a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 0], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.version_map_memory_in_bytes", + "metricAgg": "max", + "label": "Version Map", + "title": "Index Memory", + "description": "Heap memory used by Versioning (e.g., updates and deletes). This is NOT a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 5551], + [1507235530000, 5551], + [1507235540000, 5551], + [1507235550000, 6594], + [1507235560000, 6662], + [1507235570000, 6662], + [1507235580000, 6662], + [1507235590000, 6662], + [1507235600000, 6662], + [1507235610000, 7531], + [1507235620000, 7837], + [1507235630000, 7837], + [1507235640000, 7837], + [1507235650000, 7837], + [1507235660000, 7837], + [1507235670000, 9974], + [1507235680000, 9716], + [1507235690000, 9716], + [1507235700000, 9716] + ] + } + ], + "node_index_4": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.query_cache.memory_size_in_bytes", + "metricAgg": "max", + "label": "Query Cache", + "title": "Index Memory - Elasticsearch", + "description": "Heap memory used by Query Cache (e.g., cached filters). This is for the same shards, but not a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 0], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.request_cache.memory_size_in_bytes", + "metricAgg": "max", + "label": "Request Cache", + "title": "Index Memory", + "description": "Heap memory used by Request Cache (e.g., instant aggregations). This is for the same shards, but not a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 2921], + [1507235530000, 2921], + [1507235540000, 2921], + [1507235550000, 2921], + [1507235560000, 2921], + [1507235570000, 2921], + [1507235580000, 2921], + [1507235590000, 2921], + [1507235600000, 2921], + [1507235610000, 2921], + [1507235620000, 2921], + [1507235630000, 2921], + [1507235640000, 2921], + [1507235650000, 2921], + [1507235660000, 2921], + [1507235670000, 2921], + [1507235680000, 2921], + [1507235690000, 2921], + [1507235700000, 2921] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.fielddata.memory_size_in_bytes", + "metricAgg": "max", + "label": "Fielddata", + "title": "Index Memory", + "description": "Heap memory used by Fielddata (e.g., global ordinals or explicitly enabled fielddata on text fields). This is for the same shards, but not a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 0], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.segments.index_writer_memory_in_bytes", + "metricAgg": "max", + "label": "Index Writer", + "title": "Index Memory", + "description": "Heap memory used by the Index Writer. This is NOT a part of Lucene Total.", + "units": "B", + "format": "0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 153549], + [1507235530000, 153549], + [1507235540000, 153549], + [1507235550000, 849833], + [1507235560000, 156505], + [1507235570000, 156505], + [1507235580000, 156505], + [1507235590000, 156505], + [1507235600000, 156505], + [1507235610000, 3140275], + [1507235620000, 159637], + [1507235630000, 159637], + [1507235640000, 159637], + [1507235650000, 159637], + [1507235660000, 159637], + [1507235670000, 3737997], + [1507235680000, 164351], + [1507235690000, 164351], + [1507235700000, 164351] + ] + } + ], + "node_request_total": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.search.query_total", + "metricAgg": "max", + "label": "Search Total", + "title": "Request Rate", + "description": "Amount of search operations (per shard).", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0.3], + [1507235540000, 0.3], + [1507235550000, 0.3], + [1507235560000, 0.3], + [1507235570000, 0.3], + [1507235580000, 0.3], + [1507235590000, 0.4], + [1507235600000, 0.3], + [1507235610000, 0.5], + [1507235620000, 0.3], + [1507235630000, 0.3], + [1507235640000, 0.2], + [1507235650000, 0.3], + [1507235660000, 0.3], + [1507235670000, 0.5], + [1507235680000, 0.5], + [1507235690000, 0.1], + [1507235700000, 0.4] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.indexing.index_total", + "metricAgg": "max", + "label": "Indexing Total", + "title": "Request Rate", + "description": "Amount of indexing operations.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0.9], + [1507235560000, 0.6], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0.9], + [1507235620000, 0.6], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 1.8], + [1507235680000, 0.8], + [1507235690000, 0], + [1507235700000, 0] + ] + } + ], + "node_index_time": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.indexing.index_time_in_millis", + "metricAgg": "max", + "label": "Index Time", + "title": "Indexing Time", + "description": "Amount of time spent on indexing operations.", + "units": "ms", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0.8], + [1507235560000, 0.7], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 1.2], + [1507235620000, 0.7], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 4.2], + [1507235680000, 2.3], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.indexing.throttle_time_in_millis", + "metricAgg": "max", + "label": "Index Throttling Time", + "title": "Indexing Time", + "description": "Amount of time spent with index throttling, which indicates slow disks on a node.", + "units": "ms", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + } + ], + "node_index_threads": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.thread_pool.write.queue", + "metricAgg": "max", + "label": "Write Queue", + "title": "Indexing Threads", + "description": "Number of index, bulk, and write operations in the queue. The bulk threadpool was renamed to write in 6.3, and the index threadpool is deprecated.", + "units": "", + "format": "0.[00]", + "hasCalculation": true, + "isDerivative": false + }, + "data": [ + [1507235520000, 0], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.thread_pool.write.rejected", + "metricAgg": "max", + "label": "Write Rejections", + "title": "Indexing Threads", + "description": "Number of index, bulk, and write operations that have been rejected, which occurs when the queue is full. The bulk threadpool was renamed to write in 6.3, and the index threadpool is deprecated.", + "units": "", + "format": "0.[00]", + "hasCalculation": true, + "isDerivative": false + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + } + ], + "node_read_threads": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.thread_pool.search.queue", + "metricAgg": "max", + "label": "Search Queue", + "title": "Read Threads", + "description": "Number of search operations in the queue (e.g., shard level searches).", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0.2], + [1507235680000, null], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.thread_pool.search.rejected", + "metricAgg": "max", + "label": "Search Rejections", + "title": "Read Threads", + "description": "Number of search operations that have been rejected, which occurs when the queue is full.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.thread_pool.get.queue", + "metricAgg": "max", + "label": "GET Queue", + "title": "Read Threads", + "description": "Number of GET operations in the queue.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.thread_pool.get.rejected", + "metricAgg": "max", + "label": "GET Rejections", + "title": "Read Threads", + "description": "Number of GET operations that have been rejected, which occurs when the queue is full.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0], + [1507235560000, 0], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + } + ], + "node_cpu_utilization": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.process.cpu.percent", + "metricAgg": "max", + "label": "CPU Utilization", + "description": "Percentage of CPU usage for the Elasticsearch process.", + "units": "%", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1507235520000, 1], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 1], + [1507235560000, 2], + [1507235570000, 0], + [1507235580000, 2], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 3], + [1507235620000, 2], + [1507235630000, 2], + [1507235640000, 0], + [1507235650000, 1], + [1507235660000, 0], + [1507235670000, 2], + [1507235680000, 2], + [1507235690000, 1], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.process.cpu.percent", + "metricAgg": "max", + "label": "Cgroup CPU Utilization", + "title": "CPU Utilization", + "description": "CPU Usage time compared to the CPU quota shown in percentage. If CPU quotas are not set, then no data will be shown.", + "units": "%", + "format": "0,0.[00]", + "hasCalculation": true, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, null], + [1507235540000, null], + [1507235550000, null], + [1507235560000, null], + [1507235570000, null], + [1507235580000, null], + [1507235590000, null], + [1507235600000, null], + [1507235610000, null], + [1507235620000, null], + [1507235630000, null], + [1507235640000, null], + [1507235650000, null], + [1507235660000, null], + [1507235670000, null], + [1507235680000, null], + [1507235690000, null], + [1507235700000, null] + ] + } + ], + "node_cgroup_cpu": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.os.cgroup.cpuacct.usage_nanos", + "metricAgg": "max", + "label": "Cgroup Usage", + "title": "Cgroup CPU Performance", + "description": "The usage, reported in nanoseconds, of the cgroup. Compare this with the throttling to discover issues.", + "units": "ns", + "format": "0,0.[0]a", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, null], + [1507235540000, null], + [1507235550000, null], + [1507235560000, null], + [1507235570000, null], + [1507235580000, null], + [1507235590000, null], + [1507235600000, null], + [1507235610000, null], + [1507235620000, null], + [1507235630000, null], + [1507235640000, null], + [1507235650000, null], + [1507235660000, null], + [1507235670000, null], + [1507235680000, null], + [1507235690000, null], + [1507235700000, null] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.os.cgroup.cpu.stat.time_throttled_nanos", + "metricAgg": "max", + "label": "Cgroup Throttling", + "title": "Cgroup CPU Performance", + "description": "The amount of throttled time, reported in nanoseconds, of the cgroup.", + "units": "ns", + "format": "0,0.[0]a", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, null], + [1507235540000, null], + [1507235550000, null], + [1507235560000, null], + [1507235570000, null], + [1507235580000, null], + [1507235590000, null], + [1507235600000, null], + [1507235610000, null], + [1507235620000, null], + [1507235630000, null], + [1507235640000, null], + [1507235650000, null], + [1507235660000, null], + [1507235670000, null], + [1507235680000, null], + [1507235690000, null], + [1507235700000, null] + ] + } + ], + "node_cgroup_stats": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods", + "metricAgg": "max", + "label": "Cgroup Elapsed Periods", + "title": "Cgroup CFS Stats", + "description": "The number of sampling periods from the Completely Fair Scheduler (CFS). Compare against the number of times throttled.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, null], + [1507235540000, null], + [1507235550000, null], + [1507235560000, null], + [1507235570000, null], + [1507235580000, null], + [1507235590000, null], + [1507235600000, null], + [1507235610000, null], + [1507235620000, null], + [1507235630000, null], + [1507235640000, null], + [1507235650000, null], + [1507235660000, null], + [1507235670000, null], + [1507235680000, null], + [1507235690000, null], + [1507235700000, null] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.os.cgroup.cpu.stat.number_of_times_throttled", + "metricAgg": "max", + "label": "Cgroup Throttled Count", + "title": "Cgroup CFS Stats", + "description": "The number of times that the CPU was throttled by the cgroup.", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1507235520000, null], + [1507235530000, null], + [1507235540000, null], + [1507235550000, null], + [1507235560000, null], + [1507235570000, null], + [1507235580000, null], + [1507235590000, null], + [1507235600000, null], + [1507235610000, null], + [1507235620000, null], + [1507235630000, null], + [1507235640000, null], + [1507235650000, null], + [1507235660000, null], + [1507235670000, null], + [1507235680000, null], + [1507235690000, null], + [1507235700000, null] + ] + } + ], + "node_latency": [ + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.search.query_total", + "metricAgg": "sum", + "label": "Search", + "title": "Latency", + "description": "Average latency for searching, which is time it takes to execute searches divided by number of searches submitted. This considers primary and replica shards.", + "units": "ms", + "format": "0,0.[00]", + "hasCalculation": true, + "isDerivative": false + }, + "data": [ + [1507235520000, null], + [1507235530000, 0.33333333333333337], + [1507235540000, 0], + [1507235550000, 0.33333333333333337], + [1507235560000, 0], + [1507235570000, 0.33333333333333337], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0.33333333333333337], + [1507235610000, 0], + [1507235620000, 0], + [1507235630000, 0.33333333333333337], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 0.2], + [1507235680000, 0], + [1507235690000, 0], + [1507235700000, 0] + ] + }, + { + "bucket_size": "10 seconds", + "timeRange": { + "min": 1507235508000, + "max": 1507235712000 + }, + "metric": { + "app": "elasticsearch", + "field": "node_stats.indices.indexing.index_total", + "metricAgg": "sum", + "label": "Indexing", + "title": "Latency", + "description": "Average latency for indexing documents, which is time it takes to index documents divided by number that were indexed. This considers any shard located on this node, including replicas.", + "units": "ms", + "format": "0,0.[00]", + "hasCalculation": true, + "isDerivative": false + }, + "data": [ + [1507235520000, null], + [1507235530000, 0], + [1507235540000, 0], + [1507235550000, 0.888888888888889], + [1507235560000, 1.1666666666666667], + [1507235570000, 0], + [1507235580000, 0], + [1507235590000, 0], + [1507235600000, 0], + [1507235610000, 1.3333333333333333], + [1507235620000, 1.1666666666666667], + [1507235630000, 0], + [1507235640000, 0], + [1507235650000, 0], + [1507235660000, 0], + [1507235670000, 2.3333333333333335], + [1507235680000, 2.8749999999999996], + [1507235690000, 0], + [1507235700000, 0] + ] + } + ] } } diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json index 83391e1a74616..2e71a6a1551e4 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json @@ -6,9 +6,7 @@ "dataSize": 1604573, "nodesCount": 2, "upTime": 1126458, - "version": [ - "7.0.0-alpha1" - ], + "version": ["7.0.0-alpha1"], "memUsed": 259061752, "memMax": 835321856, "unassignedShards": 0, @@ -21,7 +19,7 @@ "type": "master", "isOnline": true, "nodeTypeLabel": "Master Node", - "nodeTypeClass": "fa-star", + "nodeTypeClass": "starFilled", "shardCount": 6, "node_cgroup_quota": { "metric": { @@ -149,7 +147,7 @@ "type": "node", "isOnline": true, "nodeTypeLabel": "Node", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "shardCount": 6, "node_cgroup_throttled": { "metric": { diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json index d7a30c466c2bf..0a18664faf445 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json @@ -10,15 +10,13 @@ "totalShards": 108, "unassignedShards": 0, "upTime": 1787957, - "version": [ - "7.0.0-alpha1" - ] + "version": ["7.0.0-alpha1"] }, "nodes": [ { "isOnline": true, "name": "node01", - "nodeTypeClass": "fa-star", + "nodeTypeClass": "starFilled", "nodeTypeLabel": "Master Node", "node_cpu_utilization": { "metric": { @@ -106,7 +104,7 @@ { "isOnline": true, "name": "node02", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "node_cpu_utilization": { "metric": { diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json index 5046ddb276272..d9c04838fab10 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json @@ -10,15 +10,13 @@ "totalShards": 46, "unassignedShards": 23, "upTime": 1403187, - "version": [ - "7.0.0-alpha1" - ] + "version": ["7.0.0-alpha1"] }, "nodes": [ { "isOnline": true, "name": "whatever-01", - "nodeTypeClass": "fa-star", + "nodeTypeClass": "starFilled", "nodeTypeLabel": "Master Node", "node_cpu_utilization": { "metric": { @@ -106,7 +104,7 @@ { "isOnline": false, "name": "whatever-02", - "nodeTypeClass": "fa-server", + "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "resolver": "1jxg5T33TWub-jJL4qP0Wg", "shardCount": 0, From 7d34a113ebb5bf50006bcfe6ab2b5b334560a983 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 27 Nov 2019 14:18:32 -0700 Subject: [PATCH 119/128] [SIEM][Detection Engine] Change security model to use SIEM permissions ## Summary * Changes incorrect `access:signals-all` to be the correct `access:siem` * Adds a boom transformer to push back better error messages to the client in some cases ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../routes/create_rules_route.ts | 76 ++++++++++--------- .../routes/delete_rules_route.ts | 28 ++++--- .../routes/find_rules_route.ts | 26 ++++--- .../routes/read_rules_route.ts | 26 ++++--- .../routes/update_rules_route.ts | 74 +++++++++--------- .../lib/detection_engine/routes/utils.test.ts | 39 ++++++++++ .../lib/detection_engine/routes/utils.ts | 14 ++++ 7 files changed, 178 insertions(+), 105 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts index 4ff3a9b96b93e..2b69e57f2c2ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -14,13 +14,13 @@ import { RulesRequest } from '../alerts/types'; import { createRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { readRules } from '../alerts/read_rules'; -import { transformOrError } from './utils'; +import { transformOrError, transformError } from './utils'; export const createCreateRulesRoute: Hapi.ServerRoute = { method: 'POST', path: DETECTION_ENGINE_RULES_URL, options: { - tags: ['access:signals-all'], + tags: ['access:siem'], validate: { options: { abortEarly: false, @@ -62,42 +62,46 @@ export const createCreateRulesRoute: Hapi.ServerRoute = { return headers.response().code(404); } - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId }); - if (rule != null) { - return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); + try { + if (ruleId != null) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); + } } - } - const createdRule = await createRules({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - filter, - from, - immutable, - query, - language, - outputIndex, - savedId, - meta, - filters, - ruleId: ruleId != null ? ruleId : uuid.v4(), - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threats, - references, - }); - return transformOrError(createdRule); + const createdRule = await createRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + filter, + from, + immutable, + query, + language, + outputIndex, + savedId, + meta, + filters, + ruleId: ruleId != null ? ruleId : uuid.v4(), + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threats, + references, + }); + return transformOrError(createdRule); + } catch (err) { + return transformError(err); + } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts index 12dff0dd60c14..fe8b139f11c01 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts @@ -12,13 +12,13 @@ import { deleteRules } from '../alerts/delete_rules'; import { ServerFacade } from '../../../types'; import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; -import { getIdError, transformOrError } from './utils'; +import { getIdError, transformOrError, transformError } from './utils'; export const createDeleteRulesRoute: Hapi.ServerRoute = { method: 'DELETE', path: DETECTION_ENGINE_RULES_URL, options: { - tags: ['access:signals-all'], + tags: ['access:siem'], validate: { options: { abortEarly: false, @@ -35,17 +35,21 @@ export const createDeleteRulesRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const rule = await deleteRules({ - actionsClient, - alertsClient, - id, - ruleId, - }); + try { + const rule = await deleteRules({ + actionsClient, + alertsClient, + id, + ruleId, + }); - if (rule != null) { - return transformOrError(rule); - } else { - return getIdError({ id, ruleId }); + if (rule != null) { + return transformOrError(rule); + } else { + return getIdError({ id, ruleId }); + } + } catch (err) { + return transformError(err); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts index 893fb3f689d16..137dd9352699e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts @@ -11,13 +11,13 @@ import { findRules } from '../alerts/find_rules'; import { FindRulesRequest } from '../alerts/types'; import { findRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; -import { transformFindAlertsOrError } from './utils'; +import { transformFindAlertsOrError, transformError } from './utils'; export const createFindRulesRoute: Hapi.ServerRoute = { method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find`, options: { - tags: ['access:signals-all'], + tags: ['access:siem'], validate: { options: { abortEarly: false, @@ -34,15 +34,19 @@ export const createFindRulesRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const rules = await findRules({ - alertsClient, - perPage: query.per_page, - page: query.page, - sortField: query.sort_field, - sortOrder: query.sort_order, - filter: query.filter, - }); - return transformFindAlertsOrError(rules); + try { + const rules = await findRules({ + alertsClient, + perPage: query.per_page, + page: query.page, + sortField: query.sort_field, + sortOrder: query.sort_order, + filter: query.filter, + }); + return transformFindAlertsOrError(rules); + } catch (err) { + return transformError(err); + } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts index 4642c34fbe339..a7bda40fdc523 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts @@ -7,7 +7,7 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { getIdError, transformOrError } from './utils'; +import { getIdError, transformOrError, transformError } from './utils'; import { readRules } from '../alerts/read_rules'; import { ServerFacade } from '../../../types'; @@ -18,7 +18,7 @@ export const createReadRulesRoute: Hapi.ServerRoute = { method: 'GET', path: DETECTION_ENGINE_RULES_URL, options: { - tags: ['access:signals-all'], + tags: ['access:siem'], validate: { options: { abortEarly: false, @@ -34,15 +34,19 @@ export const createReadRulesRoute: Hapi.ServerRoute = { if (!alertsClient || !actionsClient) { return headers.response().code(404); } - const rule = await readRules({ - alertsClient, - id, - ruleId, - }); - if (rule != null) { - return transformOrError(rule); - } else { - return getIdError({ id, ruleId }); + try { + const rule = await readRules({ + alertsClient, + id, + ruleId, + }); + if (rule != null) { + return transformOrError(rule); + } else { + return getIdError({ id, ruleId }); + } + } catch (err) { + return transformError(err); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts index c5fb1675fb343..156756698435f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts @@ -11,13 +11,13 @@ import { updateRules } from '../alerts/update_rules'; import { UpdateRulesRequest } from '../alerts/types'; import { updateRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; -import { getIdError, transformOrError } from './utils'; +import { getIdError, transformOrError, transformError } from './utils'; export const createUpdateRulesRoute: Hapi.ServerRoute = { method: 'PUT', path: DETECTION_ENGINE_RULES_URL, options: { - tags: ['access:signals-all'], + tags: ['access:siem'], validate: { options: { abortEarly: false, @@ -61,39 +61,43 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const rule = await updateRules({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - filter, - from, - immutable, - query, - language, - outputIndex, - savedId, - meta, - filters, - id, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threats, - references, - }); - if (rule != null) { - return transformOrError(rule); - } else { - return getIdError({ id, ruleId }); + try { + const rule = await updateRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + filter, + from, + immutable, + query, + language, + outputIndex, + savedId, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threats, + references, + }); + if (rule != null) { + return transformOrError(rule); + } else { + return getIdError({ id, ruleId }); + } + } catch (err) { + return transformError(err); } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 4ef2d87d1d736..1461c75295ee3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -5,11 +5,13 @@ */ import Boom from 'boom'; + import { transformAlertToRule, getIdError, transformFindAlertsOrError, transformOrError, + transformError, } from './utils'; import { getResult } from './__mocks__/request_responses'; @@ -474,4 +476,41 @@ describe('utils', () => { expect((output as Boom).message).toEqual('Internal error transforming'); }); }); + + describe('transformError', () => { + test('returns boom if it is a boom object', () => { + const boom = new Boom(''); + const transformed = transformError(boom); + expect(transformed).toBe(boom); + }); + + test('returns a boom if it is some non boom object that has a statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(Boom.isBoom(transformed)).toBe(true); + }); + + test('returns a boom with the message set', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(transformed.message).toBe('some message'); + }); + + test('does not return a boom if it is some non boom object but it does not have a status Code.', () => { + const error: Error = { + name: 'some name', + message: 'some message', + }; + const transformed = transformError(error); + expect(Boom.isBoom(transformed)).toBe(false); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 947fb27a89c3a..40a33e9d97a18 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -74,3 +74,17 @@ export const transformOrError = (alert: unknown): Partial | return new Boom('Internal error transforming', { statusCode: 500 }); } }; + +export const transformError = (err: Error & { statusCode?: number }) => { + if (Boom.isBoom(err)) { + return err; + } else { + if (err.statusCode != null) { + return new Boom(err.message, { statusCode: err.statusCode }); + } else { + // natively return the err and allow the regular framework + // to deal with the error when it is a non Boom + return err; + } + } +}; From 3ed18b4e06b0c8a155880bdd14338b1e46791b4e Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Wed, 27 Nov 2019 20:07:27 -0600 Subject: [PATCH 120/128] De-angularize visLegend (#50613) * deangularize visLegend * update vislib controller to mount react legend directly * convert legend components to eui * Position popover based on legend position * Styles cleanup including removing of unused/unnecessary styles --- .../kbn_vislib_vis_types/public/controller.js | 86 +++--- .../vis_types/__tests__/vislib_vis_legend.js | 159 ---------- .../vis/vis_types/_vislib_vis_legend.scss | 96 ++---- .../vis/vis_types/vislib_vis_legend.html | 99 ------- .../public/vis/vis_types/vislib_vis_legend.js | 186 ------------ .../vislib_vis_legend.test.tsx.snap | 5 + .../vis/vis_types/vislib_vis_legend/index.ts | 21 ++ .../vis/vis_types/vislib_vis_legend/models.ts | 84 ++++++ .../vislib_vis_legend.test.tsx | 279 ++++++++++++++++++ .../vislib_vis_legend/vislib_vis_legend.tsx | 264 +++++++++++++++++ .../vislib_vis_legend_item.tsx | 203 +++++++++++++ .../functional/page_objects/visualize_page.js | 15 +- .../common/layouts/preserve_layout.css | 5 - .../export_types/common/layouts/print.css | 5 - .../translations/translations/ja-JP.json | 1 + .../translations/translations/zh-CN.json | 1 + 16 files changed, 942 insertions(+), 567 deletions(-) delete mode 100644 src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js delete mode 100644 src/legacy/ui/public/vis/vis_types/vislib_vis_legend.html delete mode 100644 src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js create mode 100644 src/legacy/ui/public/vis/vis_types/vislib_vis_legend/__snapshots__/vislib_vis_legend.test.tsx.snap create mode 100644 src/legacy/ui/public/vis/vis_types/vislib_vis_legend/index.ts create mode 100644 src/legacy/ui/public/vis/vis_types/vislib_vis_legend/models.ts create mode 100644 src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx create mode 100644 src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.tsx create mode 100644 src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend_item.tsx diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controller.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controller.js index 319f7d9b9fa9f..014606fb375ab 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controller.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controller.js @@ -19,9 +19,12 @@ import $ from 'jquery'; -import { CUSTOM_LEGEND_VIS_TYPES } from '../../../ui/public/vis/vis_types/vislib_vis_legend'; +import React from 'react'; + +import { CUSTOM_LEGEND_VIS_TYPES, VisLegend } from '../../../ui/public/vis/vis_types/vislib_vis_legend'; import { VislibVisProvider } from '../../../ui/public/vislib/vis'; import chrome from '../../../ui/public/chrome'; +import { mountReactNode } from '../../../../core/public/utils'; const legendClassName = { top: 'visLib--legend-top', @@ -30,24 +33,30 @@ const legendClassName = { right: 'visLib--legend-right', }; - export class vislibVisController { constructor(el, vis) { this.el = el; this.vis = vis; - this.$scope = null; + this.unmount = null; + this.legendRef = React.createRef(); + // vis mount point this.container = document.createElement('div'); this.container.className = 'visLib'; this.el.appendChild(this.container); + // chart mount point this.chartEl = document.createElement('div'); this.chartEl.className = 'visLib__chart'; this.container.appendChild(this.chartEl); + // legend mount point + this.legendEl = document.createElement('div'); + this.legendEl.className = 'visLib__legend'; + this.container.appendChild(this.legendEl); } render(esResponse, visParams) { - if (this.vis.vislibVis) { + if (this.vislibVis) { this.destroy(); } @@ -56,62 +65,69 @@ export class vislibVisController { const $injector = await chrome.dangerouslyGetActiveInjector(); const Private = $injector.get('Private'); this.Vislib = Private(VislibVisProvider); - this.$compile = $injector.get('$compile'); - this.$rootScope = $injector.get('$rootScope'); } if (this.el.clientWidth === 0 || this.el.clientHeight === 0) { return resolve(); } - this.vis.vislibVis = new this.Vislib(this.chartEl, visParams); - this.vis.vislibVis.on('brush', this.vis.API.events.brush); - this.vis.vislibVis.on('click', this.vis.API.events.filter); - this.vis.vislibVis.on('renderComplete', resolve); + this.vislibVis = new this.Vislib(this.chartEl, visParams); + this.vislibVis.on('brush', this.vis.API.events.brush); + this.vislibVis.on('click', this.vis.API.events.filter); + this.vislibVis.on('renderComplete', resolve); - this.vis.vislibVis.initVisConfig(esResponse, this.vis.getUiState()); + this.vislibVis.initVisConfig(esResponse, this.vis.getUiState()); if (visParams.addLegend) { $(this.container).attr('class', (i, cls) => { return cls.replace(/visLib--legend-\S+/g, ''); }).addClass(legendClassName[visParams.legendPosition]); - this.$scope = this.$rootScope.$new(); - this.$scope.refreshLegend = 0; - this.$scope.vis = this.vis; - this.$scope.visData = esResponse; - this.$scope.visParams = visParams; - this.$scope.uiState = this.$scope.vis.getUiState(); - const legendHtml = this.$compile('')(this.$scope); - this.container.appendChild(legendHtml[0]); - this.$scope.$digest(); + this.mountLegend(esResponse, visParams.legendPosition); } - this.vis.vislibVis.render(esResponse, this.vis.getUiState()); + this.vislibVis.render(esResponse, this.vis.getUiState()); // refreshing the legend after the chart is rendered. // this is necessary because some visualizations // provide data necessary for the legend only after a render cycle. - if (visParams.addLegend && CUSTOM_LEGEND_VIS_TYPES.includes(this.vis.vislibVis.visConfigArgs.type)) { - this.$scope.refreshLegend++; - this.$scope.$digest(); - - this.vis.vislibVis.render(esResponse, this.vis.getUiState()); + if (visParams.addLegend && CUSTOM_LEGEND_VIS_TYPES.includes(this.vislibVis.visConfigArgs.type)) { + this.unmountLegend(); + this.mountLegend(esResponse, visParams.legendPosition); + this.vislibVis.render(esResponse, this.vis.getUiState()); } }); } + mountLegend(visData, position) { + this.unmount = mountReactNode( + + )(this.legendEl); + } + + unmountLegend() { + if (this.unmount) { + this.unmount(); + } + } + destroy() { - if (this.vis.vislibVis) { - this.vis.vislibVis.off('brush', this.vis.API.events.brush); - this.vis.vislibVis.off('click', this.vis.API.events.filter); - this.vis.vislibVis.destroy(); - delete this.vis.vislibVis; + if (this.unmount) { + this.unmount(); } - $(this.container).find('vislib-legend').remove(); - if (this.$scope) { - this.$scope.$destroy(); - this.$scope = null; + + if (this.vislibVis) { + this.vislibVis.off('brush', this.vis.API.events.brush); + this.vislibVis.off('click', this.vis.API.events.filter); + this.vislibVis.destroy(); + delete this.vislibVis; } } } diff --git a/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js b/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js deleted file mode 100644 index 4ad579e1e45f9..0000000000000 --- a/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { Vis } from '../../../../../core_plugins/visualizations/public/np_ready/public/vis'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('visualize_legend directive', function () { - let $rootScope; - let $compile; - let $timeout; - let $el; - let indexPattern; - let fixtures; - - beforeEach(ngMock.module('kibana', 'kibana/table_vis')); - beforeEach(ngMock.inject(function (Private, $injector) { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - $timeout = $injector.get('$timeout'); - fixtures = require('fixtures/fake_hierarchical_data'); - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - })); - - // basically a parameterized beforeEach - function init(vis, esResponse) { - vis.aggs.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); }); - - $rootScope.vis = vis; - $rootScope.visData = esResponse; - $rootScope.uiState = require('fixtures/mock_ui_state'); - $el = $(''); - $compile($el)($rootScope); - $rootScope.$apply(); - } - - function CreateVis(params, requiresSearch) { - const vis = new Vis(indexPattern, { - type: 'line', - params: params || {}, - aggs: [ - { type: 'count', schema: 'metric' }, - { - type: 'range', - schema: 'bucket', - params: { - field: 'bytes', - ranges: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 } - ] - } - } - ] - }); - - vis.type.requestHandler = requiresSearch ? 'default' : 'none'; - vis.type.responseHandler = 'none'; - vis.type.requiresSearch = false; - return vis; - } - - it('calls highlight handler when highlight function is called', () => { - const requiresSearch = false; - const vis = new CreateVis(null, requiresSearch); - init(vis, fixtures.oneRangeBucket); - let highlight = 0; - _.set(vis, 'vislibVis.handler.highlight', () => { highlight++; }); - $rootScope.highlight({ currentTarget: null }); - expect(highlight).to.equal(1); - }); - - it('calls unhighlight handler when unhighlight function is called', () => { - const requiresSearch = false; - const vis = new CreateVis(null, requiresSearch); - init(vis, fixtures.oneRangeBucket); - let unhighlight = 0; - _.set(vis, 'vislibVis.handler.unHighlight', () => { unhighlight++; }); - $rootScope.unhighlight({ currentTarget: null }); - expect(unhighlight).to.equal(1); - }); - - describe('setColor function', () => { - beforeEach(() => { - const requiresSearch = false; - const vis = new CreateVis(null, requiresSearch); - init(vis, fixtures.oneRangeBucket); - }); - - it('sets the color in the UI state', () => { - $rootScope.setColor('test', '#ffffff'); - const colors = $rootScope.uiState.get('vis.colors'); - expect(colors.test).to.equal('#ffffff'); - }); - }); - - describe('toggleLegend function', () => { - let vis; - - beforeEach(() => { - const requiresSearch = false; - vis = new CreateVis(null, requiresSearch); - init(vis, fixtures.oneRangeBucket); - }); - - it('sets the color in the UI state', () => { - $rootScope.open = true; - $rootScope.toggleLegend(); - $rootScope.$digest(); - $timeout.flush(); - $timeout.verifyNoPendingTasks(); - let legendOpen = $rootScope.uiState.get('vis.legendOpen'); - expect(legendOpen).to.equal(false); - - $rootScope.toggleLegend(); - $rootScope.$digest(); - $timeout.flush(); - $timeout.verifyNoPendingTasks(); - legendOpen = $rootScope.uiState.get('vis.legendOpen'); - expect(legendOpen).to.equal(true); - }); - }); - - it('does not update scope.data if visData is null', () => { - $rootScope.visData = null; - $rootScope.$digest(); - expect($rootScope.data).to.not.equal(null); - }); - - it('works without handler set', () => { - const requiresSearch = false; - const vis = new CreateVis(null, requiresSearch); - vis.vislibVis = {}; - init(vis, fixtures.oneRangeBucket); - expect(() => { - $rootScope.highlight({ currentTarget: null }); - $rootScope.unhighlight({ currentTarget: null }); - }).to.not.throwError(); - }); -}); diff --git a/src/legacy/ui/public/vis/vis_types/_vislib_vis_legend.scss b/src/legacy/ui/public/vis/vis_types/_vislib_vis_legend.scss index 8de88959cfb59..4d7c0e2bdcadb 100644 --- a/src/legacy/ui/public/vis/vis_types/_vislib_vis_legend.scss +++ b/src/legacy/ui/public/vis/vis_types/_vislib_vis_legend.scss @@ -11,9 +11,11 @@ $visLegendLineHeight: $euiSize; position: absolute; bottom: 0; left: 0; + display: flex; + padding: $euiSizeXS; background-color: $euiColorEmptyShade; transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance, - background-color $euiAnimSpeedFast $euiAnimSlightResistance $euiAnimSpeedExtraSlow; + background-color $euiAnimSpeedFast $euiAnimSlightResistance $euiAnimSpeedExtraSlow; &:focus { box-shadow: none; @@ -22,13 +24,11 @@ $visLegendLineHeight: $euiSize; } .visLegend__toggle--isOpen { - background-color: transparentize($euiColorDarkestShade, .9); + background-color: transparentize($euiColorDarkestShade, 0.9); opacity: 1; } - .visLegend { - @include euiFontSizeXS; display: flex; min-height: 0; height: 100%; @@ -46,27 +46,30 @@ $visLegendLineHeight: $euiSize; } } -/** - * 1. Position the .visLegend__valueDetails absolutely against the legend item - * 2. Make sure the .visLegend__valueDetails is visible outside the list bounds - * 3. Make sure the currently selected item is top most in z level - */ .visLegend__list { @include euiScrollBar; display: flex; - line-height: $visLegendLineHeight; width: $visLegendWidth; // Must be a hard-coded width for the chart to get its correct dimensions flex: 1 1 auto; flex-direction: column; overflow-x: hidden; overflow-y: auto; + .visLegend__button { + font-size: $euiFontSizeXS; + text-align: left; + overflow: hidden; // Ensures scrollbars don't appear because EuiButton__text has a high line-height + + .visLegend__valueTitle { + vertical-align: middle; + } + } + .visLib--legend-top &, .visLib--legend-bottom & { width: auto; flex-direction: row; flex-wrap: wrap; - overflow: visible; /* 2 */ .visLegend__value { flex-grow: 0; @@ -79,74 +82,19 @@ $visLegendLineHeight: $euiSize; } } -.visLegend__value { - cursor: pointer; - padding: $euiSizeXS; - display: flex; - flex-shrink: 0; - position: relative; /* 1 */ - - > * { - width: 100%; - } - - &.disabled { - opacity: 0.5; - } +.visLegend__valueColorPicker { + width: ($euiSizeL * 8); // 8 columns } -.visLegend__valueTitle { - @include euiTextTruncate; // ALWAYS truncate - color: $visTextColor; +.visLegend__valueColorPickerDot { + cursor: pointer; &:hover { - text-decoration: underline; - } -} - -.visLegend__valueTitle--full ~ .visLegend__valueDetails { - z-index: 2; /* 3 */ -} - -.visLegend__valueDetails { - background-color: $euiColorEmptyShade; - - .visLib--legend-left &, - .visLib--legend-right & { - margin-top: $euiSizeXS; - border-bottom: $euiBorderThin; - } - - .visLib--legend-top &, - .visLib--legend-bottom & { - @include euiBottomShadowMedium; - position: absolute; /* 1 */ - border-radius: $euiBorderRadius; + transform: scale(1.4); } - .visLib--legend-bottom & { - bottom: $visLegendLineHeight + 2 * $euiSizeXS; - } - - .visLib--legend-top & { - margin-top: $euiSizeXS; - } -} - -.visLegend__valueColorPicker { - width: $visColorPickerWidth; - margin: auto; - - .visLegend__valueColorPickerDot { - $colorPickerDotsPerRow: 8; - $colorPickerDotMargin: $euiSizeXS / 2; - $colorPickerDotWidth: $visColorPickerWidth / $colorPickerDotsPerRow - 2 * $colorPickerDotMargin; - - margin: $colorPickerDotMargin; - width: $colorPickerDotWidth; - - &:hover { - transform: scale(1.4); - } + &-isSelected { + border: $euiSizeXS solid; + border-radius: 100%; } } diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.html b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.html deleted file mode 100644 index 70d2a796658f2..0000000000000 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.html +++ /dev/null @@ -1,99 +0,0 @@ -

    - -
      - -
    • - -
      -
      - - {{legendData.label}} -
      - -
      -
      - - - -
      - -
      - - - - -
      - -
      -
      - -
    • -
    -
    diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js deleted file mode 100644 index 3d054b8f8a2fb..0000000000000 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import html from './vislib_vis_legend.html'; -import { Data } from '../../vislib/lib/data'; -import { uiModules } from '../../modules'; -import { createFiltersFromEvent } from '../../../../core_plugins/visualizations/public'; -import { htmlIdGenerator, keyCodes } from '@elastic/eui'; -import { getTableAggs } from '../../visualize/loader/pipeline_helpers/utilities'; - -export const CUSTOM_LEGEND_VIS_TYPES = ['heatmap', 'gauge']; - -uiModules.get('kibana') - .directive('vislibLegend', function ($timeout) { - - return { - restrict: 'E', - template: html, - link: function ($scope) { - $scope.legendId = htmlIdGenerator()('legend'); - $scope.open = $scope.uiState.get('vis.legendOpen', true); - - $scope.$watch('visData', function (data) { - if (!data) return; - $scope.data = data; - }); - - $scope.$watch('refreshLegend', () => { - refresh(); - }); - - $scope.highlight = function (event) { - const el = event.currentTarget; - const handler = $scope.vis.vislibVis.handler; - - //there is no guarantee that a Chart will set the highlight-function on its handler - if (!handler || typeof handler.highlight !== 'function') { - return; - } - handler.highlight.call(el, handler.el); - }; - - $scope.unhighlight = function (event) { - const el = event.currentTarget; - const handler = $scope.vis.vislibVis.handler; - //there is no guarantee that a Chart will set the unhighlight-function on its handler - if (!handler || typeof handler.unHighlight !== 'function') { - return; - } - handler.unHighlight.call(el, handler.el); - }; - - $scope.setColor = function (label, color) { - const colors = $scope.uiState.get('vis.colors') || {}; - if (colors[label] === color) delete colors[label]; - else colors[label] = color; - $scope.uiState.setSilent('vis.colors', null); - $scope.uiState.set('vis.colors', colors); - $scope.uiState.emit('colorChanged'); - refresh(); - }; - - $scope.toggleLegend = function () { - const bwcAddLegend = $scope.vis.params.addLegend; - const bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend; - $scope.open = !$scope.uiState.get('vis.legendOpen', bwcLegendStateDefault); - // open should be applied on template before we update uiState - $timeout(() => { - $scope.uiState.set('vis.legendOpen', $scope.open); - }); - }; - - $scope.filter = function (legendData, negate) { - $scope.vis.API.events.filter({ data: legendData.values, negate: negate }); - }; - - $scope.canFilter = function (legendData) { - if (CUSTOM_LEGEND_VIS_TYPES.includes($scope.vis.vislibVis.visConfigArgs.type)) { - return false; - } - const filters = createFiltersFromEvent({ aggConfigs: $scope.tableAggs, data: legendData.values }); - return filters.length; - }; - - /** - * Keydown listener for a legend entry. - * This will close the details panel of this legend entry when pressing Escape. - */ - $scope.onLegendEntryKeydown = function (event) { - if (event.keyCode === keyCodes.ESCAPE) { - event.preventDefault(); - event.stopPropagation(); - $scope.shownDetails = undefined; - } - }; - - $scope.toggleDetails = function (label) { - $scope.shownDetails = $scope.shownDetails === label ? undefined : label; - }; - - $scope.areDetailsVisible = function (label) { - return $scope.shownDetails === label; - }; - - $scope.colors = [ - '#3F6833', '#967302', '#2F575E', '#99440A', '#58140C', '#052B51', '#511749', '#3F2B5B', //6 - '#508642', '#CCA300', '#447EBC', '#C15C17', '#890F02', '#0A437C', '#6D1F62', '#584477', //2 - '#629E51', '#E5AC0E', '#64B0C8', '#E0752D', '#BF1B00', '#0A50A1', '#962D82', '#614D93', //4 - '#7EB26D', '#EAB839', '#6ED0E0', '#EF843C', '#E24D42', '#1F78C1', '#BA43A9', '#705DA0', // Normal - '#9AC48A', '#F2C96D', '#65C5DB', '#F9934E', '#EA6460', '#5195CE', '#D683CE', '#806EB7', //5 - '#B7DBAB', '#F4D598', '#70DBED', '#F9BA8F', '#F29191', '#82B5D8', '#E5A8E2', '#AEA2E0', //3 - '#E0F9D7', '#FCEACA', '#CFFAFF', '#F9E2D2', '#FCE2DE', '#BADFF4', '#F9D9F9', '#DEDAF7' //7 - ]; - - function refresh() { - const vislibVis = $scope.vis.vislibVis; - if (!vislibVis || !vislibVis.visConfig) { - $scope.labels = [{ label: i18n.translate('common.ui.vis.visTypes.legend.loadingLabel', { defaultMessage: 'loading…' }) }]; - return; - } // make sure vislib is defined at this point - - if ($scope.uiState.get('vis.legendOpen') == null && $scope.vis.params.addLegend != null) { - $scope.open = $scope.vis.params.addLegend; - } - - if (CUSTOM_LEGEND_VIS_TYPES.includes(vislibVis.visConfigArgs.type)) { - const labels = vislibVis.getLegendLabels(); - if (labels) { - $scope.labels = _.map(labels, label => { - return { label: label }; - }); - } - } else { - $scope.labels = getLabels($scope.data, vislibVis.visConfigArgs.type); - } - - if (vislibVis.visConfig) { - $scope.getColor = vislibVis.visConfig.data.getColorFunc(); - } - - $scope.tableAggs = getTableAggs($scope.vis); - } - - // Most of these functions were moved directly from the old Legend class. Not a fan of this. - function getLabels(data, type) { - if (!data) return []; - data = data.columns || data.rows || [data]; - if (type === 'pie') return Data.prototype.pieNames(data); - return getSeriesLabels(data); - } - - function getSeriesLabels(data) { - const values = data.map(function (chart) { - return chart.series; - }) - .reduce(function (a, b) { - return a.concat(b); - }, []); - return _.compact(_.uniq(values, 'label')).map(label => { - return { - ...label, - values: [label.values[0].seriesRaw], - }; - }); - } - } - }; - }); diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/__snapshots__/vislib_vis_legend.test.tsx.snap b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/__snapshots__/vislib_vis_legend.test.tsx.snap new file mode 100644 index 0000000000000..f2c9f4e1b53ec --- /dev/null +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/__snapshots__/vislib_vis_legend.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VisLegend Component Legend closed should match the snapshot 1`] = `"
    "`; + +exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
    "`; diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/index.ts b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/index.ts new file mode 100644 index 0000000000000..ebf132f0ab697 --- /dev/null +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { VisLegend } from './vislib_vis_legend'; +export { CUSTOM_LEGEND_VIS_TYPES } from './models'; diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/models.ts b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/models.ts new file mode 100644 index 0000000000000..1c8d5baf011a3 --- /dev/null +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/models.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface LegendItem { + label: string; + values: any[]; +} + +export const CUSTOM_LEGEND_VIS_TYPES = ['heatmap', 'gauge']; + +export const legendColors: string[] = [ + '#3F6833', + '#967302', + '#2F575E', + '#99440A', + '#58140C', + '#052B51', + '#511749', + '#3F2B5B', // 6 + '#508642', + '#CCA300', + '#447EBC', + '#C15C17', + '#890F02', + '#0A437C', + '#6D1F62', + '#584477', // 2 + '#629E51', + '#E5AC0E', + '#64B0C8', + '#E0752D', + '#BF1B00', + '#0A50A1', + '#962D82', + '#614D93', // 4 + '#7EB26D', + '#EAB839', + '#6ED0E0', + '#EF843C', + '#E24D42', + '#1F78C1', + '#BA43A9', + '#705DA0', // Normal + '#9AC48A', + '#F2C96D', + '#65C5DB', + '#F9934E', + '#EA6460', + '#5195CE', + '#D683CE', + '#806EB7', // 5 + '#B7DBAB', + '#F4D598', + '#70DBED', + '#F9BA8F', + '#F29191', + '#82B5D8', + '#E5A8E2', + '#AEA2E0', // 3 + '#E0F9D7', + '#FCEACA', + '#CFFAFF', + '#F9E2D2', + '#FCE2DE', + '#BADFF4', + '#F9D9F9', + '#DEDAF7', // 7 +]; diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx new file mode 100644 index 0000000000000..ba47de73964f6 --- /dev/null +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx @@ -0,0 +1,279 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-hooks-testing-library'; + +import { I18nProvider } from '@kbn/i18n/react'; +import { EuiButtonGroup } from '@elastic/eui'; + +import { VisLegend, VisLegendProps } from '../vislib_vis_legend/vislib_vis_legend'; +import { legendColors } from './models'; + +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + htmlIdGenerator: jest.fn().mockReturnValue(() => 'legendId'), +})); + +jest.mock('../../../visualize/loader/pipeline_helpers/utilities', () => ({ + getTableAggs: jest.fn(), +})); +jest.mock('../../../../../core_plugins/visualizations/public', () => ({ + createFiltersFromEvent: jest.fn().mockReturnValue(['yes']), +})); + +const vis = { + params: { + addLegend: true, + }, + API: { + events: { + filter: jest.fn(), + }, + }, +}; +const vislibVis = { + handler: { + highlight: jest.fn(), + unHighlight: jest.fn(), + }, + getLegendLabels: jest.fn(), + visConfigArgs: { + type: 'area', + }, + visConfig: { + data: { + getColorFunc: jest.fn().mockReturnValue(() => 'red'), + }, + }, +}; + +const visData = { + series: [ + { + label: 'A', + values: [ + { + seriesRaw: 'valuesA', + }, + ], + }, + { + label: 'B', + values: [ + { + seriesRaw: 'valuesB', + }, + ], + }, + ], +}; + +const mockState = new Map(); +const uiState = { + get: jest + .fn() + .mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)), + set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)), + emit: jest.fn(), + setSilent: jest.fn(), +}; + +const getWrapper = (props?: Partial) => + mount( + + + + ); + +const getLegendItems = (wrapper: ReactWrapper) => wrapper.find('.visLegend__button'); + +describe('VisLegend Component', () => { + let wrapper: ReactWrapper; + + afterEach(() => { + mockState.clear(); + jest.clearAllMocks(); + }); + + describe('Legend open', () => { + beforeEach(() => { + mockState.set('vis.legendOpen', true); + wrapper = getWrapper(); + }); + + it('should match the snapshot', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('Legend closed', () => { + beforeEach(() => { + mockState.set('vis.legendOpen', false); + wrapper = getWrapper(); + }); + + it('should match the snapshot', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('Highlighting', () => { + beforeEach(() => { + wrapper = getWrapper(); + }); + + it('should call highlight handler when legend item is focused', () => { + const first = getLegendItems(wrapper).first(); + first.simulate('focus'); + + expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); + }); + + it('should call highlight handler when legend item is hovered', () => { + const first = getLegendItems(wrapper).first(); + first.simulate('mouseEnter'); + + expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); + }); + + it('should call unHighlight handler when legend item is blurred', () => { + let first = getLegendItems(wrapper).first(); + first.simulate('focus'); + first = getLegendItems(wrapper).first(); + first.simulate('blur'); + + expect(vislibVis.handler.unHighlight).toHaveBeenCalledTimes(1); + }); + + it('should call unHighlight handler when legend item is unhovered', () => { + const first = getLegendItems(wrapper).first(); + + act(() => { + first.simulate('mouseEnter'); + first.simulate('mouseLeave'); + }); + + expect(vislibVis.handler.unHighlight).toHaveBeenCalledTimes(1); + }); + + it('should work with no handlers set', () => { + const newVis = { + ...vis, + vislibVis: { + ...vislibVis, + handler: null, + }, + }; + + expect(() => { + wrapper = getWrapper({ vis: newVis }); + const first = getLegendItems(wrapper).first(); + first.simulate('focus'); + first.simulate('blur'); + }).not.toThrow(); + }); + }); + + describe('Filtering', () => { + beforeEach(() => { + wrapper = getWrapper(); + }); + + it('should filter out when clicked', () => { + const first = getLegendItems(wrapper).first(); + first.simulate('click'); + const filterGroup = wrapper.find(EuiButtonGroup).first(); + filterGroup.getElement().props.onChange('filterIn'); + + expect(vis.API.events.filter).toHaveBeenCalledWith({ data: ['valuesA'], negate: false }); + expect(vis.API.events.filter).toHaveBeenCalledTimes(1); + }); + + it('should filter in when clicked', () => { + const first = getLegendItems(wrapper).first(); + first.simulate('click'); + const filterGroup = wrapper.find(EuiButtonGroup).first(); + filterGroup.getElement().props.onChange('filterOut'); + + expect(vis.API.events.filter).toHaveBeenCalledWith({ data: ['valuesA'], negate: true }); + expect(vis.API.events.filter).toHaveBeenCalledTimes(1); + }); + }); + + describe('Toggles details', () => { + beforeEach(() => { + wrapper = getWrapper(); + }); + + it('should show details when clicked', () => { + const first = getLegendItems(wrapper).first(); + first.simulate('click'); + + expect(wrapper.exists('.visLegend__valueDetails')).toBe(true); + }); + }); + + describe('setColor', () => { + beforeEach(() => { + wrapper = getWrapper(); + }); + + it('sets the color in the UI state', () => { + const first = getLegendItems(wrapper).first(); + first.simulate('click'); + + const popover = wrapper.find('.visLegend__valueDetails').first(); + const firstColor = popover.find('.visLegend__valueColorPickerDot').first(); + firstColor.simulate('click'); + + const colors = mockState.get('vis.colors'); + + expect(colors.A).toBe(legendColors[0]); + }); + }); + + describe('toggleLegend function', () => { + it('click should show legend once toggled from hidden', () => { + mockState.set('vis.legendOpen', false); + wrapper = getWrapper(); + const toggleButton = wrapper.find('.visLegend__toggle').first(); + toggleButton.simulate('click'); + + expect(wrapper.exists('.visLegend__list')).toBe(true); + }); + + it('click should hide legend once toggled from shown', () => { + mockState.set('vis.legendOpen', true); + wrapper = getWrapper(); + const toggleButton = wrapper.find('.visLegend__toggle').first(); + toggleButton.simulate('click'); + + expect(wrapper.exists('.visLegend__list')).toBe(false); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.tsx b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.tsx new file mode 100644 index 0000000000000..f0100e369f050 --- /dev/null +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.tsx @@ -0,0 +1,264 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { BaseSyntheticEvent, KeyboardEvent, PureComponent } from 'react'; +import classNames from 'classnames'; +import { compact, uniq, map } from 'lodash'; + +import { i18n } from '@kbn/i18n'; +import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; + +// @ts-ignore +import { Data } from '../../../vislib/lib/data'; +// @ts-ignore +import { createFiltersFromEvent } from '../../../../../core_plugins/visualizations/public'; +import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; +import { VisLegendItem } from './vislib_vis_legend_item'; +import { getTableAggs } from '../../../visualize/loader/pipeline_helpers/utilities'; + +export interface VisLegendProps { + vis: any; + vislibVis: any; + visData: any; + uiState: any; + position: 'top' | 'bottom' | 'left' | 'right'; +} + +export interface VisLegendState { + open: boolean; + labels: any[]; + tableAggs: any[]; + selectedLabel: string | null; +} + +export class VisLegend extends PureComponent { + legendId = htmlIdGenerator()('legend'); + getColor: (label: string) => string = () => ''; + + constructor(props: VisLegendProps) { + super(props); + const open = props.uiState.get('vis.legendOpen', true); + + this.state = { + open, + labels: [], + tableAggs: [], + selectedLabel: null, + }; + } + + componentDidMount() { + this.refresh(); + } + + toggleLegend = () => { + const bwcAddLegend = this.props.vis.params.addLegend; + const bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend; + const newOpen = !this.props.uiState.get('vis.legendOpen', bwcLegendStateDefault); + this.setState({ open: newOpen }); + // open should be applied on template before we update uiState + setTimeout(() => { + this.props.uiState.set('vis.legendOpen', newOpen); + }); + }; + + setColor = (label: string, color: string) => (event: BaseSyntheticEvent) => { + if ((event as KeyboardEvent).keyCode && (event as KeyboardEvent).keyCode !== keyCodes.ENTER) { + return; + } + + const colors = this.props.uiState.get('vis.colors') || {}; + if (colors[label] === color) delete colors[label]; + else colors[label] = color; + this.props.uiState.setSilent('vis.colors', null); + this.props.uiState.set('vis.colors', colors); + this.props.uiState.emit('colorChanged'); + this.refresh(); + }; + + filter = ({ values: data }: LegendItem, negate: boolean) => { + this.props.vis.API.events.filter({ data, negate }); + }; + + canFilter = (item: LegendItem): boolean => { + if (CUSTOM_LEGEND_VIS_TYPES.includes(this.props.vislibVis.visConfigArgs.type)) { + return false; + } + const filters = createFiltersFromEvent({ aggConfigs: this.state.tableAggs, data: item.values }); + return Boolean(filters.length); + }; + + toggleDetails = (label: string | null) => (event?: BaseSyntheticEvent) => { + if ( + event && + (event as KeyboardEvent).keyCode && + (event as KeyboardEvent).keyCode !== keyCodes.ENTER + ) { + return; + } + this.setState({ selectedLabel: this.state.selectedLabel === label ? null : label }); + }; + + getSeriesLabels = (data: any[]) => { + const values = data.map(chart => chart.series).reduce((a, b) => a.concat(b), []); + + return compact(uniq(values, 'label')).map((label: any) => ({ + ...label, + values: [label.values[0].seriesRaw], + })); + }; + + // Most of these functions were moved directly from the old Legend class. Not a fan of this. + getLabels = (data: any, type: string) => { + if (!data) return []; + data = data.columns || data.rows || [data]; + + if (type === 'pie') return Data.prototype.pieNames(data); + + return this.getSeriesLabels(data); + }; + + refresh = () => { + const vislibVis = this.props.vislibVis; + if (!vislibVis || !vislibVis.visConfig) { + this.setState({ + labels: [ + { + label: i18n.translate('common.ui.vis.visTypes.legend.loadingLabel', { + defaultMessage: 'loading…', + }), + }, + ], + }); + return; + } // make sure vislib is defined at this point + + if ( + this.props.uiState.get('vis.legendOpen') == null && + this.props.vis.params.addLegend != null + ) { + this.setState({ open: this.props.vis.params.addLegend }); + } + + if (CUSTOM_LEGEND_VIS_TYPES.includes(vislibVis.visConfigArgs.type)) { + const legendLabels = this.props.vislibVis.getLegendLabels(); + if (legendLabels) { + this.setState({ + labels: map(legendLabels, label => { + return { label }; + }), + }); + } + } else { + this.setState({ labels: this.getLabels(this.props.visData, vislibVis.visConfigArgs.type) }); + } + + if (vislibVis.visConfig) { + this.getColor = this.props.vislibVis.visConfig.data.getColorFunc(); + } + + this.setState({ tableAggs: getTableAggs(this.props.vis) }); + }; + + highlight = (event: BaseSyntheticEvent) => { + const el = event.currentTarget; + const handler = this.props.vislibVis && this.props.vislibVis.handler; + + // there is no guarantee that a Chart will set the highlight-function on its handler + if (!handler || typeof handler.highlight !== 'function') { + return; + } + handler.highlight.call(el, handler.el); + }; + + unhighlight = (event: BaseSyntheticEvent) => { + const el = event.currentTarget; + const handler = this.props.vislibVis && this.props.vislibVis.handler; + + // there is no guarantee that a Chart will set the unhighlight-function on its handler + if (!handler || typeof handler.unHighlight !== 'function') { + return; + } + handler.unHighlight.call(el, handler.el); + }; + + getAnchorPosition = () => { + const { position } = this.props; + + switch (position) { + case 'bottom': + return 'upCenter'; + case 'left': + return 'rightUp'; + case 'right': + return 'leftUp'; + default: + return 'downCenter'; + } + }; + + renderLegend = (anchorPosition: EuiPopoverProps['anchorPosition']) => ( +
      + {this.state.labels.map(item => ( + + ))} +
    + ); + + render() { + const { open } = this.state; + const anchorPosition = this.getAnchorPosition(); + + return ( +
    + + {open && this.renderLegend(anchorPosition)} +
    + ); + } +} diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend_item.tsx b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend_item.tsx new file mode 100644 index 0000000000000..7376fabfe738b --- /dev/null +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend_item.tsx @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { memo, BaseSyntheticEvent, KeyboardEvent } from 'react'; +import classNames from 'classnames'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiPopover, + keyCodes, + EuiIcon, + EuiSpacer, + EuiButtonEmpty, + EuiPopoverProps, + EuiButtonGroup, + EuiButtonGroupOption, +} from '@elastic/eui'; + +import { legendColors, LegendItem } from './models'; + +interface Props { + item: LegendItem; + legendId: string; + selected: boolean; + canFilter: boolean; + anchorPosition: EuiPopoverProps['anchorPosition']; + onFilter: (item: LegendItem, negate: boolean) => void; + onSelect: (label: string | null) => (event?: BaseSyntheticEvent) => void; + onHighlight: (event: BaseSyntheticEvent) => void; + onUnhighlight: (event: BaseSyntheticEvent) => void; + setColor: (label: string, color: string) => (event: BaseSyntheticEvent) => void; + getColor: (label: string) => string; +} + +const VisLegendItemComponent = ({ + item, + legendId, + selected, + canFilter, + anchorPosition, + onFilter, + onSelect, + onHighlight, + onUnhighlight, + setColor, + getColor, +}: Props) => { + /** + * Keydown listener for a legend entry. + * This will close the details panel of this legend entry when pressing Escape. + */ + const onLegendEntryKeydown = (event: KeyboardEvent) => { + if (event.keyCode === keyCodes.ESCAPE) { + event.preventDefault(); + event.stopPropagation(); + onSelect(null)(); + } + }; + + const filterOptions: EuiButtonGroupOption[] = [ + { + id: 'filterIn', + label: i18n.translate('common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel', { + defaultMessage: 'Filter for value {legendDataLabel}', + values: { legendDataLabel: item.label }, + }), + iconType: 'plusInCircle', + 'data-test-subj': `legend-${item.label}-filterIn`, + }, + { + id: 'filterOut', + label: i18n.translate('common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel', { + defaultMessage: 'Filter out value {legendDataLabel}', + values: { legendDataLabel: item.label }, + }), + iconType: 'minusInCircle', + 'data-test-subj': `legend-${item.label}-filterOut`, + }, + ]; + + const handleFilterChange = (id: string) => { + onFilter(item, id !== 'filterIn'); + }; + + const renderFilterBar = () => ( + <> + + + + ); + + const button = ( + + + {item.label} + + ); + + const renderDetails = () => ( + +
    + {canFilter && renderFilterBar()} + +
    + + + + {legendColors.map(color => ( + + ))} +
    +
    +
    + ); + + return ( +
  1. + {renderDetails()} +
  2. + ); +}; + +export const VisLegendItem = memo(VisLegendItemComponent); diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index 81d26a4b69478..a6792670fdb3f 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -1186,8 +1186,13 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli } async getLegendEntries() { - const legendEntries = await find.allByCssSelector('.visLegend__valueTitle', defaultFindTimeout * 2); - return await Promise.all(legendEntries.map(async chart => await chart.getAttribute('data-label'))); + const legendEntries = await find.allByCssSelector( + '.visLegend__button', + defaultFindTimeout * 2 + ); + return await Promise.all( + legendEntries.map(async chart => await chart.getAttribute('data-label')) + ); } async openLegendOptionColors(name) { @@ -1217,7 +1222,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli async toggleLegend(show = true) { await retry.try(async () => { - const isVisible = find.byCssSelector('vislib-legend'); + const isVisible = find.byCssSelector('.visLegend'); if ((show && !isVisible) || (!show && isVisible)) { await testSubjects.click('vislibToggleLegend'); } @@ -1227,7 +1232,9 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli async filterLegend(name) { await this.toggleLegend(); await testSubjects.click(`legend-${name}`); - await testSubjects.click(`legend-${name}-filterIn`); + const filters = await testSubjects.find(`legend-${name}-filters`); + const [filterIn] = await filters.findAllByCssSelector(`input`); + await filterIn.click(); await this.waitForVisualizationRenderingStabilized(); } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css b/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css index 9e8415a1ff18c..ab88e4780936e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css @@ -92,11 +92,6 @@ visualize-app .visEditor__canvas { display: none; } -/* slightly increate legend text size for readability */ -.visualize visualize-legend .visLegend__valueTitle { - font-size: 1.2em; -} - /* Ensure the min-height of the small breakpoint isn't used */ .vis-editor visualization { min-height: 0 !important; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css index 30c253f36840a..8aca042144b3b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css @@ -91,11 +91,6 @@ visualize-app .visEditor__canvas { display: none; } -/* slightly increate legend text size for readability */ -.visualize visualize-legend .visLegend__valueTitle { - font-size: 1.2em; -} - /* Ensure the min-height of the small breakpoint isn't used */ .vis-editor visualization { min-height: 0 !important; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 217b20797492a..23ebdf0ad6aad 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -529,6 +529,7 @@ "common.ui.vis.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、Elasticsearch と Kibana の {defaultDistribution} にアップグレードしてください。{ems} でより多くのズームレベルが利用できます。または、独自のマップサーバーを構成できます。詳細は { wms } または { configSettings} をご覧ください。", "common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", "common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", + "common.ui.vis.visTypes.legend.filterOptionsLegend": "{legendDataLabel}, フィルターオプション", "common.ui.vis.visTypes.legend.loadingLabel": "読み込み中…", "common.ui.vis.visTypes.legend.setColorScreenReaderDescription": "値 {legendDataLabel} の色を設定", "common.ui.vis.visTypes.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6a2ba20af7714..28f6c2857d51d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -530,6 +530,7 @@ "common.ui.vis.kibanaMap.zoomWarning": "已达到缩放级别最大数目。要一直放大,请升级到 Elasticsearch 和 Kibana 的 {defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", "common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", "common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", + "common.ui.vis.visTypes.legend.filterOptionsLegend": "{legendDataLabel}, 篩選器選項", "common.ui.vis.visTypes.legend.loadingLabel": "正在加载……", "common.ui.vis.visTypes.legend.setColorScreenReaderDescription": "为值 {legendDataLabel} 设置颜色", "common.ui.vis.visTypes.legend.toggleLegendButtonAriaLabel": "切换图例", From 60896e80a646fe9f95fa5f8ba17708663ce1e063 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:58:41 -0500 Subject: [PATCH 121/128] [SIEM] [Detection Engine] Add edit on rule creation (#51670) * Add creation rule on Detection Engine * review + bug fixes * review II + clean up * fix persistence saved query * fix eui prop + add type security to add rule * fix more bug from review III * review IV * add edit on creation on rule * review * fix status icon color * fix filter label translation --- .../components/add_item_form/index.tsx | 76 +++++---- .../description_step/filter_label.tsx | 93 +++++++++++ .../description_step/filter_operator.tsx | 119 ++++++++++++++ .../components/description_step/index.tsx | 155 ++++++++++++++++++ .../description_step/translations.tsx | 19 +++ .../components/query_bar/index.tsx | 2 +- .../create_rule/components/shared_imports.ts | 5 + .../components/status_icon/index.tsx | 5 +- .../step_about_rule/default_value.ts | 9 +- .../components/step_about_rule/index.tsx | 25 +-- .../components/step_about_rule/schema.tsx | 7 +- .../components/step_define_rule/index.tsx | 48 +++--- .../components/step_define_rule/schema.tsx | 14 +- .../components/step_schedule_rule/index.tsx | 23 ++- .../components/step_schedule_rule/schema.tsx | 3 +- .../detection_engine/create_rule/index.tsx | 112 ++++++++++--- .../create_rule/translations.ts | 4 + .../detection_engine/create_rule/types.ts | 42 +++-- 18 files changed, 634 insertions(+), 127 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx index 6673262a15906..04bca0cdbd61b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; -import { isEmpty, isEqual } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; @@ -21,15 +21,22 @@ interface AddItemProps { export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [items, setItems] = useState(['']); - const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(false); + // const [items, setItems] = useState(['']); + const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1); - const lastInputRef = useRef(null); + const inputsRef = useRef([]); const removeItem = useCallback( (index: number) => { const values = field.value as string[]; field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + inputsRef.current = [ + ...inputsRef.current.slice(0, index), + ...inputsRef.current.slice(index + 1), + ]; + if (inputsRef.current[index] != null) { + inputsRef.current[index].value = 're-render'; + } }, [field] ); @@ -38,16 +45,26 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad const values = field.value as string[]; if (!isEmpty(values[values.length - 1])) { field.setValue([...values, '']); + } else { + field.setValue(['']); } }, [field]); const updateItem = useCallback( (event: ChangeEvent, index: number) => { + event.persist(); const values = field.value as string[]; const value = event.target.value; if (isEmpty(value)) { - setHaveBeenKeyboardDeleted(true); field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + inputsRef.current = [ + ...inputsRef.current.slice(0, index), + ...inputsRef.current.slice(index + 1), + ]; + setHaveBeenKeyboardDeleted(inputsRef.current.length - 1); + if (inputsRef.current[index] != null) { + inputsRef.current[index].value = 're-render'; + } } else { field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); } @@ -56,31 +73,30 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad ); const handleLastInputRef = useCallback( - (element: HTMLInputElement | null) => { - lastInputRef.current = element; + (index: number, element: HTMLInputElement | null) => { + if (element != null) { + inputsRef.current = [ + ...inputsRef.current.slice(0, index), + element, + ...inputsRef.current.slice(index + 1), + ]; + } }, - [lastInputRef] + [inputsRef] ); useEffect(() => { - if (!isEqual(field.value, items)) { - setItems( - isEmpty(field.value) - ? [''] - : haveBeenKeyboardDeleted - ? [...(field.value as string[]), ''] - : (field.value as string[]) - ); - setHaveBeenKeyboardDeleted(false); - } - }, [field.value]); - - useEffect(() => { - if (!haveBeenKeyboardDeleted && lastInputRef != null && lastInputRef.current != null) { - lastInputRef.current.focus(); + if ( + haveBeenKeyboardDeleted !== -1 && + !isEmpty(inputsRef.current) && + inputsRef.current[haveBeenKeyboardDeleted] != null + ) { + inputsRef.current[haveBeenKeyboardDeleted].focus(); + setHaveBeenKeyboardDeleted(-1); } - }, [haveBeenKeyboardDeleted, lastInputRef]); + }, [haveBeenKeyboardDeleted, inputsRef.current]); + const values = field.value as string[]; return ( <> - {items.map((item, index) => { + {values.map((item, index) => { const euiFieldProps = { disabled: isDisabled, - ...(index === items.length - 1 ? { inputRef: handleLastInputRef } : {}), + ...(index === values.length - 1 + ? { inputRef: handleLastInputRef.bind(null, index) } + : {}), + ...(inputsRef.current[index] != null && inputsRef.current[index].value !== item + ? { value: item } + : {}), }; return (
    @@ -109,13 +130,12 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad aria-label={I18n.DELETE} /> } - value={item} onChange={e => updateItem(e, index)} compressed fullWidth {...euiFieldProps} /> - {items.length - 1 !== index && } + {values.length - 1 !== index && }
    ); })} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx new file mode 100644 index 0000000000000..15844f5012291 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; +import { existsOperator, isOneOfOperator } from './filter_operator'; + +interface Props { + filter: esFilters.Filter; + valueLabel?: string; +} + +export const FilterLabel = memo(({ filter, valueLabel }) => { + const prefixText = filter.meta.negate + ? ` ${i18n.translate('xpack.siem.detectionEngine.createRule.filterLabel.negatedFilterPrefix', { + defaultMessage: 'NOT ', + })}` + : ''; + const prefix = + filter.meta.negate && !filter.meta.disabled ? ( + {prefixText} + ) : ( + prefixText + ); + + if (filter.meta.alias !== null) { + return ( + <> + {prefix} + {filter.meta.alias} + + ); + } + + switch (filter.meta.type) { + case esFilters.FILTERS.EXISTS: + return ( + <> + {prefix} + {`${filter.meta.key}: ${existsOperator.message}`} + + ); + case esFilters.FILTERS.GEO_BOUNDING_BOX: + return ( + <> + {prefix} + {`${filter.meta.key}: ${valueLabel}`} + + ); + case esFilters.FILTERS.GEO_POLYGON: + return ( + <> + {prefix} + {`${filter.meta.key}: ${valueLabel}`} + + ); + case esFilters.FILTERS.PHRASES: + return ( + <> + {prefix} + {filter.meta.key} {isOneOfOperator.message} {valueLabel} + + ); + case esFilters.FILTERS.QUERY_STRING: + return ( + <> + {prefix} + {valueLabel} + + ); + case esFilters.FILTERS.PHRASE: + case esFilters.FILTERS.RANGE: + return ( + <> + {prefix} + {`${filter.meta.key}: ${valueLabel}`} + + ); + default: + return ( + <> + {prefix} + {JSON.stringify(filter.query)} + + ); + } +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx new file mode 100644 index 0000000000000..7aa5b0beed2d6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; + +export interface Operator { + message: string; + type: esFilters.FILTERS; + negate: boolean; + fieldTypes?: string[]; +} + +export const isOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isOperatorOptionLabel', + { + defaultMessage: 'is', + } + ), + type: esFilters.FILTERS.PHRASE, + negate: false, +}; + +export const isNotOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isNotOperatorOptionLabel', + { + defaultMessage: 'is not', + } + ), + type: esFilters.FILTERS.PHRASE, + negate: true, +}; + +export const isOneOfOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isOneOfOperatorOptionLabel', + { + defaultMessage: 'is one of', + } + ), + type: esFilters.FILTERS.PHRASES, + negate: false, + fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], +}; + +export const isNotOneOfOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isNotOneOfOperatorOptionLabel', + { + defaultMessage: 'is not one of', + } + ), + type: esFilters.FILTERS.PHRASES, + negate: true, + fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], +}; + +export const isBetweenOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isBetweenOperatorOptionLabel', + { + defaultMessage: 'is between', + } + ), + type: esFilters.FILTERS.RANGE, + negate: false, + fieldTypes: ['number', 'date', 'ip'], +}; + +export const isNotBetweenOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.isNotBetweenOperatorOptionLabel', + { + defaultMessage: 'is not between', + } + ), + type: esFilters.FILTERS.RANGE, + negate: true, + fieldTypes: ['number', 'date', 'ip'], +}; + +export const existsOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.existsOperatorOptionLabel', + { + defaultMessage: 'exists', + } + ), + type: esFilters.FILTERS.EXISTS, + negate: false, +}; + +export const doesNotExistOperator = { + message: i18n.translate( + 'xpack.siem.detectionEngine.createRule.filterLabel.doesNotExistOperatorOptionLabel', + { + defaultMessage: 'does not exist', + } + ), + type: esFilters.FILTERS.EXISTS, + negate: true, +}; + +export const FILTER_OPERATORS: Operator[] = [ + isOperator, + isNotOperator, + isOneOfOperator, + isNotOneOfOperator, + isBetweenOperator, + isNotBetweenOperator, + existsOperator, + doesNotExistOperator, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx new file mode 100644 index 0000000000000..3e8147e5ca3c1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui'; +import { isEmpty, chunk, get, pick } from 'lodash/fp'; +import React, { memo, ReactNode } from 'react'; +import styled from 'styled-components'; + +import { + IIndexPattern, + esFilters, + Query, + utils, +} from '../../../../../../../../../../src/plugins/data/public'; + +import { FilterLabel } from './filter_label'; +import { FormSchema } from '../shared_imports'; +import * as I18n from './translations'; + +interface StepRuleDescriptionProps { + data: unknown; + indexPatterns?: IIndexPattern; + schema: FormSchema; +} + +const EuiBadgeWrap = styled(EuiBadge)` + .euiBadge__text { + white-space: pre-wrap !important; + } +`; + +const EuiFlexItemWidth = styled(EuiFlexItem)` + width: 50%; +`; + +export const StepRuleDescription = memo( + ({ data, indexPatterns, schema }) => { + const keys = Object.keys(schema); + return ( + + {chunk(keys.includes('queryBar') ? 3 : Math.ceil(keys.length / 2), keys).map(key => ( + + + + ))} + + ); + } +); + +interface ListItems { + title: NonNullable; + description: NonNullable; +} + +const buildListItems = ( + data: unknown, + schema: FormSchema, + indexPatterns?: IIndexPattern +): ListItems[] => + Object.keys(schema).reduce( + (acc, field) => [ + ...acc, + ...getDescriptionItem(field, get([field, 'label'], schema), data, indexPatterns), + ], + [] + ); + +const getDescriptionItem = ( + field: string, + label: string, + value: unknown, + indexPatterns?: IIndexPattern +): ListItems[] => { + if (field === 'queryBar' && indexPatterns != null) { + const filters = get('queryBar.filters', value) as esFilters.Filter[]; + const query = get('queryBar.query', value) as Query; + const savedId = get('queryBar.saved_id', value); + let items: ListItems[] = []; + if (!isEmpty(filters)) { + items = [ + ...items, + { + title: <>{I18n.FILTERS_LABEL}, + description: ( + + {filters.map((filter, index) => ( + + + + + + ))} + + ), + }, + ]; + } + if (!isEmpty(query.query)) { + items = [ + ...items, + { + title: <>{I18n.QUERY_LABEL}, + description: <>{query.query}, + }, + ]; + } + if (!isEmpty(savedId)) { + items = [ + ...items, + { + title: <>{I18n.SAVED_ID_LABEL}, + description: <>{savedId}, + }, + ]; + } + return items; + } else if (field === 'description') { + return [ + { + title: label, + description: , + }, + ]; + } else if (Array.isArray(get(field, value))) { + return [ + { + title: label, + description: ( + + {get(field, value).map((val: string) => ( + + {val} + + ))} + + ), + }, + ]; + } + return [ + { + title: label, + description: get(field, value), + }, + ]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx new file mode 100644 index 0000000000000..0995e0e916652 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const FILTERS_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.filtersLabel', { + defaultMessage: 'Filters', +}); + +export const QUERY_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.QueryLabel', { + defaultMessage: 'Query', +}); + +export const SAVED_ID_LABEL = i18n.translate('xpack.siem.detectionEngine.createRule.savedIdLabel', { + defaultMessage: 'Saved query name', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx index 4e7832c890255..8db9d3b44e3f5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx @@ -28,7 +28,7 @@ import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; export interface FieldValueQueryBar { filters: esFilters.Filter[]; query: Query; - saved_id: string; + saved_id: string | null; } interface QueryBarDefineRuleProps { dataTestSubj: string; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts index 6c91c4a02edf9..8eb85c9fe3fae 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts @@ -8,9 +8,14 @@ export { getUseField, getFieldValidityAndErrorMessage, FieldHook, + FIELD_TYPES, Form, FormDataProvider, + FormSchema, UseField, useForm, + ValidationFunc, } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { Field } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx index ad0011ff8ed18..22b116557ae6e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx @@ -9,8 +9,7 @@ import React, { memo } from 'react'; import styled from 'styled-components'; import { useEuiTheme } from '../../../../../lib/theme/use_eui_theme'; - -export type RuleStatusType = 'passive' | 'active' | 'valid'; +import { RuleStatusType } from '../../types'; export interface RuleStatusIconProps { name: string; @@ -32,7 +31,7 @@ export const RuleStatusIcon = memo(({ name, type }) => { return ( - {type === 'valid' ? : null} + {type === 'valid' ? : null} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts index b94fa8c933937..7c4d78f364479 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export const defaultValue = { +import { AboutStepRule } from '../../types'; + +export const defaultValue: AboutStepRule = { name: '', description: '', + isNew: true, severity: 'low', riskScore: 50, - references: [], - falsePositives: [], + references: [''], + falsePositives: [''], tags: [], }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx index 4393f39ad2f85..56830f252748f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx @@ -5,9 +5,9 @@ */ import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useState } from 'react'; -import { RuleStepProps, RuleStep } from '../../types'; +import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; import * as CreateRuleI18n from '../../translations'; import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { AddItem } from '../add_item_form'; @@ -15,24 +15,29 @@ import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './da import { defaultValue } from './default_value'; import { schema } from './schema'; import * as I18n from './translations'; +import { StepRuleDescription } from '../description_step'; const CommonUseField = getUseField({ component: Field }); -export const StepAboutRule = memo(({ isLoading, setStepData }) => { +export const StepAboutRule = memo(({ isEditView, isLoading, setStepData }) => { + const [myStepData, setMyStepData] = useState(defaultValue); const { form } = useForm({ - schema, - defaultValue, + defaultValue: myStepData, options: { stripEmptyFields: false }, + schema, }); const onSubmit = useCallback(async () => { - const { isValid: newIsValid, data } = await form.submit(); - if (newIsValid) { - setStepData(RuleStep.aboutRule, data, newIsValid); + const { isValid, data } = await form.submit(); + if (isValid) { + setStepData(RuleStep.aboutRule, data, isValid); + setMyStepData({ ...data, isNew: false } as AboutStepRule); } }, [form]); - return ( + return isEditView && myStepData != null ? ( + + ) : ( <> (({ isLoading, setStepData }) => - {CreateRuleI18n.CONTINUE} + {myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx index 97ad3d595a938..da908bdf02e43 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx @@ -8,13 +8,8 @@ import { EuiText } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - FormSchema, - FIELD_TYPES, -} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; - import * as CreateRuleI18n from '../../translations'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../shared_imports'; const { emptyField } = fieldValidators; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx index b09d0df962793..26306d3573926 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx @@ -8,11 +8,13 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elasti import { isEqual } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useState } from 'react'; +import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules/fetch_index_patterns'; import { DEFAULT_INDEX_KEY, DEFAULT_SIGNALS_INDEX_KEY } from '../../../../../../common/constants'; import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; import * as CreateRuleI18n from '../../translations'; -import { RuleStep, RuleStepProps } from '../../types'; +import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; +import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { schema } from './schema'; @@ -20,7 +22,7 @@ import * as I18n from './translations'; const CommonUseField = getUseField({ component: Field }); -export const StepDefineRule = memo(({ isLoading, setStepData }) => { +export const StepDefineRule = memo(({ isEditView, isLoading, setStepData }) => { const [initializeOutputIndex, setInitializeOutputIndex] = useState(true); const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); const [ @@ -29,26 +31,28 @@ export const StepDefineRule = memo(({ isLoading, setStepData }) = ] = useFetchIndexPatterns(); const [indicesConfig] = useKibanaUiSetting(DEFAULT_INDEX_KEY); const [signalIndexConfig] = useKibanaUiSetting(DEFAULT_SIGNALS_INDEX_KEY); - + const [myStepData, setMyStepData] = useState({ + index: indicesConfig || [], + isNew: true, + outputIndex: signalIndexConfig, + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: null, + }, + useIndicesConfig: 'true', + }); const { form } = useForm({ schema, - defaultValue: { - index: indicesConfig || [], - outputIndex: signalIndexConfig, - queryBar: { - query: { query: '', language: 'kuery' }, - filters: [], - saved_id: null, - }, - useIndicesConfig: 'true', - }, + defaultValue: myStepData, options: { stripEmptyFields: false }, }); const onSubmit = useCallback(async () => { - const { isValid: newIsValid, data } = await form.submit(); - if (newIsValid) { - setStepData(RuleStep.defineRule, data, newIsValid); + const { isValid, data } = await form.submit(); + if (isValid) { + setStepData(RuleStep.defineRule, data, isValid); + setMyStepData({ ...data, isNew: false } as DefineStepRule); } }, [form]); @@ -60,7 +64,13 @@ export const StepDefineRule = memo(({ isLoading, setStepData }) = } }, [initializeOutputIndex, signalIndexConfig, form]); - return ( + return isEditView && myStepData != null ? ( + + ) : ( <> (({ isLoading, setStepData }) = } else if ( indexField != null && useIndicesConfig === 'false' && - !isEqual(indexField.value, []) + isEqual(indexField.value, indicesConfig) ) { indexField.setValue([]); setIndices([]); @@ -150,7 +160,7 @@ export const StepDefineRule = memo(({ isLoading, setStepData }) = - {CreateRuleI18n.CONTINUE} + {myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx index 58a9e57b32ce6..9f1644e73bf0b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx @@ -9,18 +9,18 @@ import { EuiText } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React from 'react'; -import { - FormSchema, - FIELD_TYPES, - ValidationFunc, -} from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; -import { ERROR_CODE } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; import * as CreateRuleI18n from '../../translations'; import { FieldValueQueryBar } from '../query_bar'; +import { + ERROR_CODE, + FIELD_TYPES, + fieldValidators, + FormSchema, + ValidationFunc, +} from '../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY } from './translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx index 10b95ac6c8742..bd4d5aa4f8ca1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx @@ -5,21 +5,25 @@ */ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useState } from 'react'; -import { RuleStep, RuleStepProps } from '../../types'; +import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; +import { StepRuleDescription } from '../description_step'; import { ScheduleItem } from '../schedule_item_form'; import { Form, UseField, useForm } from '../shared_imports'; import { schema } from './schema'; import * as I18n from './translations'; -export const StepScheduleRule = memo(({ isLoading, setStepData }) => { +export const StepScheduleRule = memo(({ isEditView, isLoading, setStepData }) => { + const [myStepData, setMyStepData] = useState({ + enabled: true, + interval: '5m', + isNew: true, + from: '0m', + }); const { form } = useForm({ schema, - defaultValue: { - interval: '5m', - from: '0m', - }, + defaultValue: myStepData, options: { stripEmptyFields: false }, }); @@ -28,12 +32,15 @@ export const StepScheduleRule = memo(({ isLoading, setStepData }) const { isValid: newIsValid, data } = await form.submit(); if (newIsValid) { setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid); + setMyStepData({ ...data, isNew: false } as ScheduleStepRule); } }, [form] ); - return ( + return isEditView && myStepData != null ? ( + + ) : ( <> { [RuleStep.aboutRule]: { isValid: false, data: {} }, [RuleStep.scheduleRule]: { isValid: false, data: {} }, }); + const [isStepRuleInEditView, setIsStepRuleInEditView] = useState>({ + [RuleStep.defineRule]: false, + [RuleStep.aboutRule]: false, + [RuleStep.scheduleRule]: false, + }); const [{ isLoading, isSaved }, setRule] = usePersistRule(); const setStepData = (step: RuleStep, data: unknown, isValid: boolean) => { - stepsData.current[step] = { data, isValid }; + stepsData.current[step] = { ...stepsData.current[step], data, isValid }; if (isValid) { const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); if ([0, 1].includes(stepRuleIdx)) { - openCloseAccordion(step); - openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); - setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + setIsStepRuleInEditView({ + ...isStepRuleInEditView, + [step]: true, + }); + if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { + openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + } } else if ( stepRuleIdx === 2 && stepsData.current[RuleStep.defineRule].isValid && @@ -106,6 +116,7 @@ export const CreateRuleComponent = React.memo(() => { const manageAccordions = useCallback( (id: RuleStep, isOpen: boolean) => { + const activeRuleIdx = stepsRuleOrder.findIndex(step => step === openAccordionId); const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id); const isLatestStepsRuleValid = stepRuleIdx === 0 @@ -114,23 +125,35 @@ export const CreateRuleComponent = React.memo(() => { .filter((stepRule, index) => index < stepRuleIdx) .every(stepRule => stepsData.current[stepRule].isValid); - if ( - openAccordionId != null && - openAccordionId !== id && - !stepsData.current[openAccordionId].isValid && - isOpen - ) { - openCloseAccordion(id); - } else if (!isLatestStepsRuleValid && isOpen) { + if (stepRuleIdx < activeRuleIdx && !isOpen) { openCloseAccordion(id); - } else if (openAccordionId != null && id !== openAccordionId && isOpen) { - openCloseAccordion(openAccordionId); - setOpenAccordionId(id); - } else if (openAccordionId == null && isOpen) { - setOpenAccordionId(id); + } else if (stepRuleIdx >= activeRuleIdx) { + if ( + openAccordionId != null && + openAccordionId !== id && + !stepsData.current[openAccordionId].isValid && + !isStepRuleInEditView[id] && + isOpen + ) { + openCloseAccordion(id); + } else if (!isLatestStepsRuleValid && isOpen) { + openCloseAccordion(id); + } else if (id !== openAccordionId && isOpen) { + setOpenAccordionId(id); + } } }, - [openAccordionId] + [isStepRuleInEditView, openAccordionId] + ); + + const manageIsEditable = useCallback( + (id: RuleStep) => { + setIsStepRuleInEditView({ + ...isStepRuleInEditView, + [id]: false, + }); + }, + [isStepRuleInEditView] ); if (isSaved && stepsData.current[RuleStep.scheduleRule].isValid) { @@ -154,9 +177,24 @@ export const CreateRuleComponent = React.memo(() => { paddingSize="xs" ref={defineRuleRef} onToggle={manageAccordions.bind(null, RuleStep.defineRule)} + extraAction={ + stepsData.current[RuleStep.defineRule].isValid && ( + + {`Edit`} + + ) + } > - + @@ -168,9 +206,24 @@ export const CreateRuleComponent = React.memo(() => { paddingSize="xs" ref={aboutRuleRef} onToggle={manageAccordions.bind(null, RuleStep.aboutRule)} + extraAction={ + stepsData.current[RuleStep.aboutRule].isValid && ( + + {`Edit`} + + ) + } > - + @@ -182,9 +235,24 @@ export const CreateRuleComponent = React.memo(() => { paddingSize="xs" ref={scheduleRuleRef} onToggle={manageAccordions.bind(null, RuleStep.scheduleRule)} + extraAction={ + stepsData.current[RuleStep.scheduleRule].isValid && ( + + {`Edit`} + + ) + } > - + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts index ca96566305a6b..1ef3a435bbc30 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts @@ -38,3 +38,7 @@ export const CONTINUE = i18n.translate( defaultMessage: 'Continue', } ); + +export const UPDATE = i18n.translate('xpack.siem.detectionEngine.createRule.updateButtonTitle', { + defaultMessage: 'Update', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts index a03f6a0b11bee..8c395c458e59a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts @@ -12,24 +12,46 @@ export enum RuleStep { aboutRule = 'about-rule', scheduleRule = 'schedule-rule', } +export type RuleStatusType = 'passive' | 'active' | 'valid'; export interface RuleStepData { - isValid: boolean; data: unknown; + isValid: boolean; } export interface RuleStepProps { setStepData: (step: RuleStep, data: unknown, isValid: boolean) => void; + isEditView: boolean; isLoading: boolean; } -export interface DefineStepRule { +interface StepRuleData { + isNew: boolean; +} +export interface AboutStepRule extends StepRuleData { + name: string; + description: string; + severity: string; + riskScore: number; + references: string[]; + falsePositives: string[]; + tags: string[]; +} + +export interface DefineStepRule extends StepRuleData { outputIndex: string; useIndicesConfig: string; index: string[]; queryBar: FieldValueQueryBar; } +export interface ScheduleStepRule extends StepRuleData { + enabled: boolean; + interval: string; + from: string; + to?: string; +} + export interface DefineStepRuleJson { output_index: string; index: string[]; @@ -39,16 +61,6 @@ export interface DefineStepRuleJson { language: string; } -export interface AboutStepRule { - name: string; - description: string; - severity: string; - riskScore: number; - references: string[]; - falsePositives: string[]; - tags: string[]; -} - export interface AboutStepRuleJson { name: string; description: string; @@ -59,12 +71,6 @@ export interface AboutStepRuleJson { tags: string[]; } -export interface ScheduleStepRule { - enabled: boolean; - interval: string; - from: string; - to?: string; -} export type ScheduleStepRuleJson = ScheduleStepRule; export type FormatRuleType = 'query' | 'saved_query'; From 85d438cb0371e0e58e918ebea3f0c6ba147777d0 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 28 Nov 2019 08:18:11 +0100 Subject: [PATCH 122/128] [ML] Re-activate after method in transform test (#51815) --- .../functional/apps/transform/creation_saved_search.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 8a69700bee578..4528a2c84d9de 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -23,10 +23,10 @@ export default function({ getService }: FtrProviderContext) { await esArchiver.load('ml/farequote'); }); - // after(async () => { - // await esArchiver.unload('ml/farequote'); - // await transform.api.cleanTransformIndices(); - // }); + after(async () => { + await esArchiver.unload('ml/farequote'); + await transform.api.cleanTransformIndices(); + }); const testDataList = [ { From bbd517b3cab4764e2ec3bcbe58da5c52a1164753 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 28 Nov 2019 11:22:29 +0200 Subject: [PATCH 123/128] =?UTF-8?q?Move=20saved=20queries=20service=20+=20?= =?UTF-8?q?language=20switcher=20=E2=87=92=20NP=20(#51812)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move saved queries service + language switcher to NP * test fixes * test fix * fix ts * mock search service --- src/legacy/core_plugins/data/index.ts | 2 +- src/legacy/core_plugins/data/public/index.ts | 7 ++- src/legacy/core_plugins/data/public/plugin.ts | 6 +-- .../components/query_string_input.test.tsx | 2 +- .../components/query_string_input.tsx | 2 +- .../core_plugins/data/public/search/index.ts | 1 - .../save_query_form.tsx | 7 ++- .../saved_query_list_item.tsx | 2 +- .../saved_query_management_component.tsx | 3 +- .../search_bar/components/search_bar.test.tsx | 1 + .../search_bar/components/search_bar.tsx | 8 +-- .../data/public/search/search_bar/index.tsx | 19 ------- .../kibana/public/dashboard/application.ts | 2 +- .../kibana/public/dashboard/dashboard_app.tsx | 4 +- .../dashboard/dashboard_app_controller.tsx | 4 +- .../kibana/public/dashboard/plugin.ts | 2 +- .../public/discover/angular/discover.js | 5 +- .../kibana/public/discover/kibana_services.ts | 4 +- .../kibana/public/visualize/editor/editor.js | 5 +- .../public/visualize/kibana_services.ts | 6 +-- .../new_platform/new_platform.karma_mock.js | 8 +++ src/plugins/data/public/plugin.ts | 2 +- src/plugins/data/public/query/index.tsx | 3 +- src/plugins/data/public/query/mocks.ts | 1 + .../data/public/query/query_service.ts | 9 ++-- .../data/public/query/saved_query/index.ts} | 29 +--------- .../saved_query}/saved_query_service.test.ts | 3 +- .../query/saved_query}/saved_query_service.ts | 21 +------- .../data/public/query/saved_query/types.ts | 53 +++++++++++++++++++ src/plugins/data/public/ui/index.ts | 2 + .../language_switcher.test.tsx.snap | 0 .../language_switcher.test.tsx | 2 +- .../query_string_input}/language_switcher.tsx | 2 +- .../maps/public/angular/map_controller.js | 3 +- .../public/components/query_bar/index.tsx | 2 +- .../public/components/search_bar/index.tsx | 3 +- .../components/timeline/query_bar/index.tsx | 10 ++-- .../components/query_bar/index.tsx | 4 +- .../utils/saved_query_services/index.tsx | 2 +- 39 files changed, 131 insertions(+), 120 deletions(-) rename src/{legacy/core_plugins/data/public/search/search_service.ts => plugins/data/public/query/saved_query/index.ts} (57%) rename src/{legacy/core_plugins/data/public/search/search_bar/lib => plugins/data/public/query/saved_query}/saved_query_service.test.ts (98%) rename src/{legacy/core_plugins/data/public/search/search_bar/lib => plugins/data/public/query/saved_query}/saved_query_service.ts (88%) create mode 100644 src/plugins/data/public/query/saved_query/types.ts rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui/query_string_input}/__snapshots__/language_switcher.test.tsx.snap (100%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui/query_string_input}/language_switcher.test.tsx (96%) rename src/{legacy/core_plugins/data/public/query/query_bar/components => plugins/data/public/ui/query_string_input}/language_switcher.tsx (98%) diff --git a/src/legacy/core_plugins/data/index.ts b/src/legacy/core_plugins/data/index.ts index 71f2fa5ffec7c..c91500cd545d4 100644 --- a/src/legacy/core_plugins/data/index.ts +++ b/src/legacy/core_plugins/data/index.ts @@ -20,7 +20,7 @@ import { resolve } from 'path'; import { Legacy } from '../../../../kibana'; import { mappings } from './mappings'; -import { SavedQuery } from './public'; +import { SavedQuery } from '../../../plugins/data/public'; // eslint-disable-next-line import/no-default-export export default function DataPlugin(kibana: any) { diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 184084e3cc3e6..833d8c248f46a 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -38,7 +38,12 @@ export { StaticIndexPattern, } from './index_patterns'; export { QueryStringInput } from './query'; -export { SearchBar, SearchBarProps, SavedQueryAttributes, SavedQuery } from './search'; +export { SearchBar, SearchBarProps } from './search'; +export { + SavedQueryAttributes, + SavedQuery, + SavedQueryTimeFilter, +} from '../../../../plugins/data/public'; /** @public static code */ export * from '../common'; diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index da24576655d2b..6cce91a5a25b5 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -18,7 +18,7 @@ */ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { SearchService, SearchStart, createSearchBar, StatetfulSearchBarProps } from './search'; +import { createSearchBar, StatetfulSearchBarProps } from './search'; import { IndexPatternsService, IndexPatternsSetup, IndexPatternsStart } from './index_patterns'; import { Storage, IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; @@ -51,7 +51,6 @@ export interface DataSetup { */ export interface DataStart { indexPatterns: IndexPatternsStart; - search: SearchStart; ui: { SearchBar: React.ComponentType; }; @@ -71,7 +70,6 @@ export interface DataStart { export class DataPlugin implements Plugin { private readonly indexPatterns: IndexPatternsService = new IndexPatternsService(); - private readonly search: SearchService = new SearchService(); private setupApi!: DataSetup; private storage!: IStorageWrapper; @@ -119,7 +117,6 @@ export class DataPlugin implements Plugin { return { FilterBar: () =>
    , + createSavedQueryService: () => {}, }; }); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index 6a1ef77a56653..da08165289afc 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -26,11 +26,9 @@ import { get, isEqual } from 'lodash'; import { IndexPattern } from '../../../../../data/public'; import { QueryBarTopRow } from '../../../query'; -import { SavedQuery, SavedQueryAttributes } from '../index'; import { SavedQueryMeta, SaveQueryForm } from './saved_query_management/save_query_form'; import { SavedQueryManagementComponent } from './saved_query_management/saved_query_management_component'; -import { SavedQueryService } from '../lib/saved_query_service'; -import { createSavedQueryService } from '../lib/saved_query_service'; + import { withKibana, KibanaReactContextValue, @@ -42,6 +40,10 @@ import { esFilters, TimeHistoryContract, FilterBar, + SavedQueryService, + createSavedQueryService, + SavedQuery, + SavedQueryAttributes, } from '../../../../../../../plugins/data/public'; interface SearchBarInjectedDeps { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx index f369bf997c1a9..faf6e24aa6ed5 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx @@ -17,23 +17,4 @@ * under the License. */ -import { RefreshInterval, TimeRange, Query, esFilters } from 'src/plugins/data/public'; - export * from './components'; - -export type SavedQueryTimeFilter = TimeRange & { - refreshInterval: RefreshInterval; -}; - -export interface SavedQuery { - id: string; - attributes: SavedQueryAttributes; -} - -export interface SavedQueryAttributes { - title: string; - description: string; - query: Query; - filters?: esFilters.Filter[]; - timefilter?: SavedQueryTimeFilter; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/application.ts index 9c50adeeefccb..f98a4ca53f467 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/application.ts @@ -67,7 +67,7 @@ export interface RenderDeps { uiSettings: UiSettingsClientContract; chrome: ChromeStart; addBasePath: (path: string) => string; - savedQueryService: DataStart['search']['services']['savedQueryService']; + savedQueryService: NpDataStart['query']['savedQueries']; embeddables: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx index 0ce8f2ef59fc0..26b86204b03db 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { StaticIndexPattern, SavedQuery } from 'plugins/data'; +import { StaticIndexPattern } from 'plugins/data'; import moment from 'moment'; import { Subscription } from 'rxjs'; @@ -31,7 +31,7 @@ import { import { ViewMode } from '../../../embeddable_api/public/np_ready/public'; import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard'; import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn } from './types'; -import { TimeRange, Query, esFilters } from '../../../../../../src/plugins/data/public'; +import { TimeRange, Query, esFilters, SavedQuery } from '../../../../../../src/plugins/data/public'; import { DashboardAppController } from './dashboard_app_controller'; import { RenderDeps } from './application'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index 1a0e13417d1e1..dcd25033e9d15 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -38,8 +38,8 @@ import { SavedObjectFinder, unhashUrl, } from './legacy_imports'; -import { FilterStateManager, IndexPattern, SavedQuery } from '../../../data/public'; -import { Query } from '../../../../../plugins/data/public'; +import { FilterStateManager, IndexPattern } from '../../../data/public'; +import { Query, SavedQuery } from '../../../../../plugins/data/public'; import './dashboard_empty_screen_directive'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 609bd717f3c48..cb0980c914983 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -104,7 +104,7 @@ export class DashboardPlugin implements Plugin { chrome: contextCore.chrome, addBasePath: contextCore.http.basePath.prepend, uiSettings: contextCore.uiSettings, - savedQueryService: dataStart.search.services.savedQueryService, + savedQueryService: npDataStart.query.savedQueries, embeddables, dashboardCapabilities: contextCore.application.capabilities.dashboard, localStorage: new Storage(localStorage), diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js index ba74ea069c4ab..0e92d048a65a9 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js @@ -68,16 +68,17 @@ const { share, StateProvider, timefilter, + npData, toastNotifications, uiModules, uiRoutes, } = getServices(); import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; -import { start as data } from '../../../../data/public/legacy'; import { generateFilters } from '../../../../../../plugins/data/public'; +import { start as data } from '../../../../data/public/legacy'; -const { savedQueryService } = data.search.services; +const savedQueryService = npData.query.savedQueries; const fetchStatuses = { UNINITIALIZED: 'uninitialized', diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 5a10e02ba8131..822bf69e52e0b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -34,7 +34,6 @@ import { StateProvider } from 'ui/state_management/state'; import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; -import { timefilter } from 'ui/timefilter'; // @ts-ignore import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; import { wrapInI18nContext } from 'ui/i18n'; @@ -58,7 +57,9 @@ const services = { uiSettings: npStart.core.uiSettings, uiActions: npStart.plugins.uiActions, embeddable: npStart.plugins.embeddable, + npData: npStart.plugins.data, share: npStart.plugins.share, + timefilter: npStart.plugins.data.query.timefilter.timefilter, // legacy docTitle, docViewsRegistry, @@ -70,7 +71,6 @@ const services = { SavedObjectProvider, SearchSource, StateProvider, - timefilter, uiModules, uiRoutes, wrapInI18nContext, diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index 6840c1386bee2..5410289bfc2d7 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -54,19 +54,20 @@ const { capabilities, chrome, chromeLegacy, + npData, data, docTitle, FilterBarQueryFilterProvider, getBasePath, toastNotifications, - timefilter, uiModules, uiRoutes, visualizations, share, } = getServices(); -const { savedQueryService } = data.search.services; +const savedQueryService = npData.query.savedQueries; +const { timefilter } = npData.query.timefilter; uiRoutes .when(VisualizeConstants.CREATE_PATH, { diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index e2201cdca9a57..61b1cde0dcaf9 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -36,7 +36,6 @@ import { wrapInI18nContext } from 'ui/i18n'; // @ts-ignore import { uiModules } from 'ui/modules'; import { FeatureCatalogueRegistryProvider } from 'ui/registry/feature_catalogue'; -import { timefilter } from 'ui/timefilter'; // Saved objects import { SavedObjectsClientProvider } from 'ui/saved_objects'; @@ -46,8 +45,8 @@ import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_regis import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; -import { start as data } from '../../../data/public/legacy'; import { start as embeddables } from '../../../../core_plugins/embeddable_api/public/np_ready/public/legacy'; +import { start as data } from '../../../data/public/legacy'; const services = { // new platform @@ -63,6 +62,7 @@ const services = { core: npStart.core, share: npStart.plugins.share, + npData: npStart.plugins.data, data, embeddables, visualizations, @@ -78,7 +78,7 @@ const services = { SavedObjectProvider, SavedObjectRegistryProvider, SavedObjectsClientProvider, - timefilter, + timefilter: npStart.plugins.data.query.timefilter.timefilter, uiModules, uiRoutes, wrapInI18nContext, diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index ff89ef69d53ca..e816b1858f21e 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -80,6 +80,14 @@ export const npSetup = { timefilter: sinon.fake(), history: sinon.fake(), }, + savedQueries: { + saveQuery: sinon.fake(), + getAllSavedQueries: sinon.fake(), + findSavedQueries: sinon.fake(), + getSavedQuery: sinon.fake(), + deleteSavedQuery: sinon.fake(), + getSavedQueryCount: sinon.fake(), + } }, fieldFormats: getFieldFormatsRegistry(mockUiSettings), }, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index d8c45b6786c0c..35d8edc488467 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -59,7 +59,7 @@ export class DataPublicPlugin implements Plugin { const startContract = { filterManager: jest.fn() as any, timefilter: timefilterServiceMock.createStartContract(), + savedQueries: jest.fn() as any, }; return startContract; diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 206f8ac284ec3..ebef8b8d45050 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -17,10 +17,11 @@ * under the License. */ -import { UiSettingsClientContract } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { FilterManager } from './filter_manager'; import { TimefilterService, TimefilterSetup } from './timefilter'; +import { createSavedQueryService } from './saved_query/saved_query_service'; /** * Query Service @@ -29,9 +30,8 @@ import { TimefilterService, TimefilterSetup } from './timefilter'; export interface QueryServiceDependencies { storage: IStorageWrapper; - uiSettings: UiSettingsClientContract; + uiSettings: CoreStart['uiSettings']; } - export class QueryService { filterManager!: FilterManager; timefilter!: TimefilterSetup; @@ -51,10 +51,11 @@ export class QueryService { }; } - public start() { + public start(savedObjects: CoreStart['savedObjects']) { return { filterManager: this.filterManager, timefilter: this.timefilter, + savedQueries: createSavedQueryService(savedObjects.client), }; } diff --git a/src/legacy/core_plugins/data/public/search/search_service.ts b/src/plugins/data/public/query/saved_query/index.ts similarity index 57% rename from src/legacy/core_plugins/data/public/search/search_service.ts rename to src/plugins/data/public/query/saved_query/index.ts index 90ac288912f64..f9b58e137b276 100644 --- a/src/legacy/core_plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/query/saved_query/index.ts @@ -17,30 +17,5 @@ * under the License. */ -import { SavedObjectsClientContract } from 'src/core/public'; -import { createSavedQueryService } from './search_bar/lib/saved_query_service'; - -/** - * Search Service - * @internal - */ - -export class SearchService { - public setup() { - // Service requires index patterns, which are only available in `start` - } - - public start(savedObjectsClient: SavedObjectsClientContract) { - return { - services: { - savedQueryService: createSavedQueryService(savedObjectsClient), - }, - }; - } - - public stop() {} -} - -/** @public */ - -export type SearchStart = ReturnType; +export { SavedQuery, SavedQueryAttributes, SavedQueryService, SavedQueryTimeFilter } from './types'; +export { createSavedQueryService } from './saved_query_service'; diff --git a/src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.test.ts b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts similarity index 98% rename from src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.test.ts rename to src/plugins/data/public/query/saved_query/saved_query_service.test.ts index 415da8a2c32cc..ecb3fc2d606ec 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.test.ts +++ b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts @@ -17,9 +17,8 @@ * under the License. */ -import { SavedQueryAttributes } from '../index'; import { createSavedQueryService } from './saved_query_service'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters, SavedQueryAttributes } from '../..'; const savedQueryAttributes: SavedQueryAttributes = { title: 'foo', diff --git a/src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts b/src/plugins/data/public/query/saved_query/saved_query_service.ts similarity index 88% rename from src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts rename to src/plugins/data/public/query/saved_query/saved_query_service.ts index 2668ce911c371..434efe80ecd8c 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts +++ b/src/plugins/data/public/query/saved_query/saved_query_service.ts @@ -17,9 +17,8 @@ * under the License. */ -import { SavedObjectAttributes } from 'src/core/server'; -import { SavedObjectsClientContract } from 'src/core/public'; -import { SavedQueryAttributes, SavedQuery } from '../index'; +import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/public'; +import { SavedQueryAttributes, SavedQuery, SavedQueryService } from './types'; type SerializedSavedQueryAttributes = SavedObjectAttributes & SavedQueryAttributes & { @@ -29,22 +28,6 @@ type SerializedSavedQueryAttributes = SavedObjectAttributes & }; }; -export interface SavedQueryService { - saveQuery: ( - attributes: SavedQueryAttributes, - config?: { overwrite: boolean } - ) => Promise; - getAllSavedQueries: () => Promise; - findSavedQueries: ( - searchText?: string, - perPage?: number, - activePage?: number - ) => Promise; - getSavedQuery: (id: string) => Promise; - deleteSavedQuery: (id: string) => Promise<{}>; - getSavedQueryCount: () => Promise; -} - export const createSavedQueryService = ( savedObjectsClient: SavedObjectsClientContract ): SavedQueryService => { diff --git a/src/plugins/data/public/query/saved_query/types.ts b/src/plugins/data/public/query/saved_query/types.ts new file mode 100644 index 0000000000000..c278c2476c2e7 --- /dev/null +++ b/src/plugins/data/public/query/saved_query/types.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RefreshInterval, TimeRange, Query, esFilters } from '../..'; + +export type SavedQueryTimeFilter = TimeRange & { + refreshInterval: RefreshInterval; +}; + +export interface SavedQuery { + id: string; + attributes: SavedQueryAttributes; +} + +export interface SavedQueryAttributes { + title: string; + description: string; + query: Query; + filters?: esFilters.Filter[]; + timefilter?: SavedQueryTimeFilter; +} + +export interface SavedQueryService { + saveQuery: ( + attributes: SavedQueryAttributes, + config?: { overwrite: boolean } + ) => Promise; + getAllSavedQueries: () => Promise; + findSavedQueries: ( + searchText?: string, + perPage?: number, + activePage?: number + ) => Promise; + getSavedQuery: (id: string) => Promise; + deleteSavedQuery: (id: string) => Promise<{}>; + getSavedQueryCount: () => Promise; +} diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 607f690d41c67..6fb8e260dd720 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -21,3 +21,5 @@ export { SuggestionsComponent } from './typeahead/suggestions_component'; export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { applyFiltersPopover } from './apply_filters'; +// temp export +export { QueryLanguageSwitcher } from './query_string_input/language_switcher'; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/language_switcher.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/data/public/query/query_bar/components/__snapshots__/language_switcher.test.tsx.snap rename to src/plugins/data/public/ui/query_string_input/__snapshots__/language_switcher.test.tsx.snap diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.test.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.test.tsx rename to src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx index ab210071870ca..e3ec5212abfd2 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { QueryLanguageSwitcher } from './language_switcher'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; -import { coreMock } from '../../../../../../../core/public/mocks'; +import { coreMock } from '../../../../../core/public/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; const startMock = coreMock.createStart(); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx similarity index 98% rename from src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.tsx rename to src/plugins/data/public/ui/query_string_input/language_switcher.tsx index 31b0e375eaac6..d86a8a970a8e7 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -31,7 +31,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState } from 'react'; -import { useKibana } from '../../../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../kibana_react/public'; interface Props { language: string; diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index b9354dd0a0ddd..810dc263f8b78 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -53,11 +53,10 @@ import { MAP_SAVED_OBJECT_TYPE, MAP_APP_PATH } from '../../common/constants'; -import { start as data } from '../../../../../../src/legacy/core_plugins/data/public/legacy'; import { npStart } from 'ui/new_platform'; import { esFilters } from '../../../../../../src/plugins/data/public'; -const { savedQueryService } = data.search.services; +const savedQueryService = npStart.plugins.data.query.savedQueries; const REACT_ANCHOR_DOM_ELEMENT_ID = 'react-maps-root'; diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx index 3f460560b79b5..591fe6a73359d 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -15,8 +15,8 @@ import { Query, TimeHistory, TimeRange, + SavedQueryTimeFilter, } from '../../../../../../../src/plugins/data/public'; -import { SavedQueryTimeFilter } from '../../../../../../../src/legacy/core_plugins/data/public/search'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; export interface QueryBarComponentProps { diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index 33fb2b9239a6a..710c1e230faba 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -38,11 +38,10 @@ import { TimeRange, Query, esFilters } from '../../../../../../../src/plugins/da const { ui: { SearchBar }, - search, } = data; export const siemFilterManager = npStart.plugins.data.query.filterManager; -export const savedQueryService = search.services.savedQueryService; +export const savedQueryService = npStart.plugins.data.query.savedQueries; interface SiemSearchBarRedux { end: number; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx index f24ee3155c924..a49ec1b758367 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -9,9 +9,13 @@ import React, { memo, useCallback, useState, useEffect } from 'react'; import { StaticIndexPattern } from 'ui/index_patterns'; import { Subscription } from 'rxjs'; -import { SavedQueryTimeFilter } from '../../../../../../../../src/legacy/core_plugins/data/public/search'; -import { SavedQuery } from '../../../../../../../../src/legacy/core_plugins/data/public'; -import { Query, esFilters, FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { + Query, + esFilters, + FilterManager, + SavedQuery, + SavedQueryTimeFilter, +} from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx index 8db9d3b44e3f5..e3180c150b239 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx @@ -11,12 +11,12 @@ import { StaticIndexPattern } from 'ui/index_patterns'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; -import { SavedQueryTimeFilter } from '../../../../../../../../../../src/legacy/core_plugins/data/public/search'; -import { SavedQuery } from '../../../../../../../../../../src/legacy/core_plugins/data/public'; import { esFilters, Query, FilterManager, + SavedQuery, + SavedQueryTimeFilter, } from '../../../../../../../../../../src/plugins/data/public'; import { QueryBar } from '../../../../../components/query_bar'; diff --git a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx index f1e4cf3411398..cda6882fe1714 100644 --- a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx @@ -8,7 +8,7 @@ import { useState, useEffect } from 'react'; import { SavedQueryService, createSavedQueryService, -} from '../../../../../../../src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service'; +} from '../../../../../../../src/plugins/data/public'; import { useKibanaCore } from '../../lib/compose/kibana_core'; From 066613e2a6b839ad893aa61fb3624187042b5bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 28 Nov 2019 09:23:56 +0000 Subject: [PATCH 124/128] Allow routes to define some payload config values (#50783) * Allow routes to define some payload config values * Documentation typo * Move hapi `payload` config under `body` + additional validations * Update API docs * Amend explanation in API docs * Add stream and buffer types to @kbn/config-schema * Fixes based on PR feedback: - Add 'patch' and 'options' to valid RouteMethod - Add tests for all the new flags - Allow `stream` and `buffer` schema in the body validations (findings from tests) * API documentation update * Fix type definitions * Fix the NITs in the PR comments + better typing inheritance * API docs update * Fix APM-legacy wrapper's types * Fix KibanaRequest.from type exposure of hapi in API docs * Move RouterRoute interface back to private + Expose some public docs * Update @kbn/config-schema docs --- .../kibana-plugin-server.basepath.get.md | 2 +- .../server/kibana-plugin-server.basepath.md | 4 +- .../kibana-plugin-server.basepath.set.md | 2 +- .../kibana-plugin-server.irouter.delete.md | 2 +- .../kibana-plugin-server.irouter.get.md | 2 +- .../server/kibana-plugin-server.irouter.md | 9 +- .../kibana-plugin-server.irouter.patch.md | 13 ++ .../kibana-plugin-server.irouter.post.md | 2 +- .../kibana-plugin-server.irouter.put.md | 2 +- .../kibana-plugin-server.kibanarequest.md | 4 +- ...ibana-plugin-server.kibanarequest.route.md | 2 +- ...kibana-plugin-server.kibanarequestroute.md | 6 +- ...plugin-server.kibanarequestroute.method.md | 2 +- ...lugin-server.kibanarequestroute.options.md | 2 +- ...plugin-server.kibanarequestrouteoptions.md | 13 ++ .../core/server/kibana-plugin-server.md | 7 +- .../kibana-plugin-server.requesthandler.md | 2 +- .../kibana-plugin-server.routeconfig.md | 4 +- ...ibana-plugin-server.routeconfig.options.md | 2 +- ...a-plugin-server.routeconfigoptions.body.md | 13 ++ ...kibana-plugin-server.routeconfigoptions.md | 3 +- ...n-server.routeconfigoptionsbody.accepts.md | 15 ++ ...-server.routeconfigoptionsbody.maxbytes.md | 15 ++ ...na-plugin-server.routeconfigoptionsbody.md | 23 +++ ...in-server.routeconfigoptionsbody.output.md | 15 ++ ...gin-server.routeconfigoptionsbody.parse.md | 15 ++ .../kibana-plugin-server.routecontenttype.md | 13 ++ .../kibana-plugin-server.routemethod.md | 2 +- .../kibana-plugin-server.routeregistrar.md | 4 +- .../kibana-plugin-server.routeschemas.body.md | 11 ++ .../kibana-plugin-server.routeschemas.md | 22 +++ ...ibana-plugin-server.routeschemas.params.md | 11 ++ ...kibana-plugin-server.routeschemas.query.md | 11 ++ .../kibana-plugin-server.validbodyoutput.md | 13 ++ packages/kbn-config-schema/README.md | 32 ++++ packages/kbn-config-schema/src/index.ts | 13 ++ .../kbn-config-schema/src/internals/index.ts | 28 ++++ .../__snapshots__/buffer_type.test.ts.snap | 11 ++ .../__snapshots__/stream_type.test.ts.snap | 11 ++ .../src/types/buffer_type.test.ts | 57 +++++++ .../src/types/buffer_type.ts | 34 ++++ packages/kbn-config-schema/src/types/index.ts | 2 + .../src/types/stream_type.test.ts | 71 ++++++++ .../src/types/stream_type.ts | 35 ++++ packages/kbn-config-schema/types/joi.d.ts | 1 + src/core/server/http/http_server.mocks.ts | 6 +- src/core/server/http/http_server.test.ts | 152 ++++++++++++++++++ src/core/server/http/http_server.ts | 9 +- src/core/server/http/http_service.mock.ts | 1 + src/core/server/http/index.ts | 7 +- src/core/server/http/router/error_wrapper.ts | 7 +- src/core/server/http/router/index.ts | 11 +- src/core/server/http/router/request.ts | 69 +++++--- src/core/server/http/router/route.ts | 97 ++++++++++- src/core/server/http/router/router.test.ts | 42 +++++ src/core/server/http/router/router.ts | 138 ++++++++++++---- src/core/server/index.ts | 7 +- src/core/server/server.api.md | 67 +++++--- .../apm/server/routes/create_api/index.ts | 4 +- .../server/routes/authentication/saml.test.ts | 4 +- 60 files changed, 1064 insertions(+), 120 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.irouter.patch.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequestrouteoptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptions.body.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.accepts.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.maxbytes.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.output.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.parse.md create mode 100644 docs/development/core/server/kibana-plugin-server.routecontenttype.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeschemas.body.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeschemas.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeschemas.params.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeschemas.query.md create mode 100644 docs/development/core/server/kibana-plugin-server.validbodyoutput.md create mode 100644 packages/kbn-config-schema/src/types/__snapshots__/buffer_type.test.ts.snap create mode 100644 packages/kbn-config-schema/src/types/__snapshots__/stream_type.test.ts.snap create mode 100644 packages/kbn-config-schema/src/types/buffer_type.test.ts create mode 100644 packages/kbn-config-schema/src/types/buffer_type.ts create mode 100644 packages/kbn-config-schema/src/types/stream_type.test.ts create mode 100644 packages/kbn-config-schema/src/types/stream_type.ts diff --git a/docs/development/core/server/kibana-plugin-server.basepath.get.md b/docs/development/core/server/kibana-plugin-server.basepath.get.md index 2b3b6c899e8de..6ef7022f10e62 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.get.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.get.md @@ -9,5 +9,5 @@ returns `basePath` value, specific for an incoming request. Signature: ```typescript -get: (request: KibanaRequest | LegacyRequest) => string; +get: (request: KibanaRequest | LegacyRequest) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.md b/docs/development/core/server/kibana-plugin-server.basepath.md index 478e29696966c..77f50abc60369 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.md @@ -16,11 +16,11 @@ export declare class BasePath | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [get](./kibana-plugin-server.basepath.get.md) | | (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest) => string | returns basePath value, specific for an incoming request. | +| [get](./kibana-plugin-server.basepath.get.md) | | (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest) => string | returns basePath value, specific for an incoming request. | | [prepend](./kibana-plugin-server.basepath.prepend.md) | | (path: string) => string | Prepends path with the basePath. | | [remove](./kibana-plugin-server.basepath.remove.md) | | (path: string) => string | Removes the prepended basePath from the path. | | [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request | -| [set](./kibana-plugin-server.basepath.set.md) | | (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | +| [set](./kibana-plugin-server.basepath.set.md) | | (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | ## Remarks diff --git a/docs/development/core/server/kibana-plugin-server.basepath.set.md b/docs/development/core/server/kibana-plugin-server.basepath.set.md index 1272a134ef5c4..56a7f644d34cc 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.set.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.set.md @@ -9,5 +9,5 @@ sets `basePath` value, specific for an incoming request. Signature: ```typescript -set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; +set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.delete.md b/docs/development/core/server/kibana-plugin-server.irouter.delete.md index 5202e0cfd5ebb..a479c03ecede3 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.delete.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.delete.md @@ -9,5 +9,5 @@ Register a route handler for `DELETE` request. Signature: ```typescript -delete: RouteRegistrar; +delete: RouteRegistrar<'delete'>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.get.md b/docs/development/core/server/kibana-plugin-server.irouter.get.md index 32552a49cb999..0d52ef26f008c 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.get.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.get.md @@ -9,5 +9,5 @@ Register a route handler for `GET` request. Signature: ```typescript -get: RouteRegistrar; +get: RouteRegistrar<'get'>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md index b5d3c893d745d..73e96191e02e7 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.md @@ -16,10 +16,11 @@ export interface IRouter | Property | Type | Description | | --- | --- | --- | -| [delete](./kibana-plugin-server.irouter.delete.md) | RouteRegistrar | Register a route handler for DELETE request. | -| [get](./kibana-plugin-server.irouter.get.md) | RouteRegistrar | Register a route handler for GET request. | +| [delete](./kibana-plugin-server.irouter.delete.md) | RouteRegistrar<'delete'> | Register a route handler for DELETE request. | +| [get](./kibana-plugin-server.irouter.get.md) | RouteRegistrar<'get'> | Register a route handler for GET request. | | [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | -| [post](./kibana-plugin-server.irouter.post.md) | RouteRegistrar | Register a route handler for POST request. | -| [put](./kibana-plugin-server.irouter.put.md) | RouteRegistrar | Register a route handler for PUT request. | +| [patch](./kibana-plugin-server.irouter.patch.md) | RouteRegistrar<'patch'> | Register a route handler for PATCH request. | +| [post](./kibana-plugin-server.irouter.post.md) | RouteRegistrar<'post'> | Register a route handler for POST request. | +| [put](./kibana-plugin-server.irouter.put.md) | RouteRegistrar<'put'> | Register a route handler for PUT request. | | [routerPath](./kibana-plugin-server.irouter.routerpath.md) | string | Resulted path | diff --git a/docs/development/core/server/kibana-plugin-server.irouter.patch.md b/docs/development/core/server/kibana-plugin-server.irouter.patch.md new file mode 100644 index 0000000000000..460f1b9d23640 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.patch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [patch](./kibana-plugin-server.irouter.patch.md) + +## IRouter.patch property + +Register a route handler for `PATCH` request. + +Signature: + +```typescript +patch: RouteRegistrar<'patch'>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.post.md b/docs/development/core/server/kibana-plugin-server.irouter.post.md index cd655c9ce0dc8..a2ac27ebc731a 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.post.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.post.md @@ -9,5 +9,5 @@ Register a route handler for `POST` request. Signature: ```typescript -post: RouteRegistrar; +post: RouteRegistrar<'post'>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.put.md b/docs/development/core/server/kibana-plugin-server.irouter.put.md index e553d4b79dd2b..219c5d8805661 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.put.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.put.md @@ -9,5 +9,5 @@ Register a route handler for `PUT` request. Signature: ```typescript -put: RouteRegistrar; +put: RouteRegistrar<'put'>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index b2460cd58f7a7..bc805fdc0b86f 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -9,7 +9,7 @@ Kibana specific abstraction for an incoming request. Signature: ```typescript -export declare class KibanaRequest +export declare class KibanaRequest ``` ## Constructors @@ -26,7 +26,7 @@ export declare class KibanaRequestHeaders | Readonly copy of incoming request headers. | | [params](./kibana-plugin-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | -| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute> | matched route details | +| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<Method>> | matched route details | | [socket](./kibana-plugin-server.kibanarequest.socket.md) | | IKibanaSocket | | | [url](./kibana-plugin-server.kibanarequest.url.md) | | Url | a WHATWG URL standard object. | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md index 88954eedf4cfb..1905070a99068 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md @@ -9,5 +9,5 @@ matched route details Signature: ```typescript -readonly route: RecursiveReadonly; +readonly route: RecursiveReadonly>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md index b92fe45d19edb..2983639458200 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md @@ -9,14 +9,14 @@ Request specific route information exposed to a handler. Signature: ```typescript -export interface KibanaRequestRoute +export interface KibanaRequestRoute ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [method](./kibana-plugin-server.kibanarequestroute.method.md) | RouteMethod | 'patch' | 'options' | | -| [options](./kibana-plugin-server.kibanarequestroute.options.md) | Required<RouteConfigOptions> | | +| [method](./kibana-plugin-server.kibanarequestroute.method.md) | Method | | +| [options](./kibana-plugin-server.kibanarequestroute.options.md) | KibanaRequestRouteOptions<Method> | | | [path](./kibana-plugin-server.kibanarequestroute.path.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md index c003b06e128e4..5775d28b1e053 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md @@ -7,5 +7,5 @@ Signature: ```typescript -method: RouteMethod | 'patch' | 'options'; +method: Method; ``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md index 98c898449a5b1..438263f61eb20 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md @@ -7,5 +7,5 @@ Signature: ```typescript -options: Required; +options: KibanaRequestRouteOptions; ``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestrouteoptions.md b/docs/development/core/server/kibana-plugin-server.kibanarequestrouteoptions.md new file mode 100644 index 0000000000000..f48711ac11f92 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestrouteoptions.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequestRouteOptions](./kibana-plugin-server.kibanarequestrouteoptions.md) + +## KibanaRequestRouteOptions type + +Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. + +Signature: + +```typescript +export declare type KibanaRequestRouteOptions = Method extends 'get' | 'options' ? Required, 'body'>> : Required>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 38c7ad75d1db9..17c5136fdc318 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -84,6 +84,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | +| [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) | Additional body options for a route | +| [RouteSchemas](./kibana-plugin-server.routeschemas.md) | RouteSchemas contains the schemas for validating the different parts of a request. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | | [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) | A reference to another saved object. | @@ -133,6 +135,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Variable | Description | | --- | --- | | [kibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Set of helpers used to create KibanaResponse to form HTTP response on an incoming request. Should be returned as a result of [RequestHandler](./kibana-plugin-server.requesthandler.md) execution. | +| [validBodyOutput](./kibana-plugin-server.validbodyoutput.md) | The set of valid body.output | ## Type Aliases @@ -156,6 +159,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. | | [ISavedObjectsRepository](./kibana-plugin-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) | | [IScopedClusterClient](./kibana-plugin-server.iscopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md). | +| [KibanaRequestRouteOptions](./kibana-plugin-server.kibanarequestrouteoptions.md) | Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. | | [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. | | [KnownHeaders](./kibana-plugin-server.knownheaders.md) | Set of well-known HTTP headers. | | [LifecycleResponseFactory](./kibana-plugin-server.lifecycleresponsefactory.md) | Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. | @@ -176,8 +180,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ResponseError](./kibana-plugin-server.responseerror.md) | Error message and optional data send to the client in case of error. | | [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. | +| [RouteContentType](./kibana-plugin-server.routecontenttype.md) | The set of supported parseable Content-Types | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | -| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Handler to declare a route. | +| [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Route handler common definition | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | diff --git a/docs/development/core/server/kibana-plugin-server.requesthandler.md b/docs/development/core/server/kibana-plugin-server.requesthandler.md index 035d16c9fca3c..79abfd4293e9f 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandler.md @@ -9,7 +9,7 @@ A function executed when route path matched requested resource path. Request han Signature: ```typescript -export declare type RequestHandler

    = (context: RequestHandlerContext, request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory) => IKibanaResponse | Promise>; +export declare type RequestHandler

    | Type, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest, TypeOf, TypeOf, Method>, response: KibanaResponseFactory) => IKibanaResponse | Promise>; ``` ## Example diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.md b/docs/development/core/server/kibana-plugin-server.routeconfig.md index 769d0dda42644..1970b23c7ec09 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfig.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.md @@ -9,14 +9,14 @@ Route specific configuration. Signature: ```typescript -export interface RouteConfig

    +export interface RouteConfig

    | Type, Method extends RouteMethod> ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [options](./kibana-plugin-server.routeconfig.options.md) | RouteConfigOptions | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). | +| [options](./kibana-plugin-server.routeconfig.options.md) | RouteConfigOptions<Method> | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). | | [path](./kibana-plugin-server.routeconfig.path.md) | string | The endpoint \_within\_ the router path to register the route. | | [validate](./kibana-plugin-server.routeconfig.validate.md) | RouteSchemas<P, Q, B> | false | A schema created with @kbn/config-schema that every request will be validated against. | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.options.md b/docs/development/core/server/kibana-plugin-server.routeconfig.options.md index 12ca36da6de7c..90ad294457101 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfig.options.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.options.md @@ -9,5 +9,5 @@ Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfig Signature: ```typescript -options?: RouteConfigOptions; +options?: RouteConfigOptions; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.body.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.body.md new file mode 100644 index 0000000000000..fee5528ce3378 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.body.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [body](./kibana-plugin-server.routeconfigoptions.body.md) + +## RouteConfigOptions.body property + +Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md). + +Signature: + +```typescript +body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md index b4d210ac0b711..99339db81065c 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -9,7 +9,7 @@ Additional route options. Signature: ```typescript -export interface RouteConfigOptions +export interface RouteConfigOptions ``` ## Properties @@ -17,5 +17,6 @@ export interface RouteConfigOptions | Property | Type | Description | | --- | --- | --- | | [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | +| [body](./kibana-plugin-server.routeconfigoptions.body.md) | Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody | Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md). | | [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.accepts.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.accepts.md new file mode 100644 index 0000000000000..f48c9a1d73b11 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.accepts.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) > [accepts](./kibana-plugin-server.routeconfigoptionsbody.accepts.md) + +## RouteConfigOptionsBody.accepts property + +A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed above will not enable them to be parsed, and if parse is true, the request will result in an error response. + +Default value: allows parsing of the following mime types: \* application/json \* application/\*+json \* application/octet-stream \* application/x-www-form-urlencoded \* multipart/form-data \* text/\* + +Signature: + +```typescript +accepts?: RouteContentType | RouteContentType[] | string | string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.maxbytes.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.maxbytes.md new file mode 100644 index 0000000000000..3d22dc07d5bae --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.maxbytes.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) > [maxBytes](./kibana-plugin-server.routeconfigoptionsbody.maxbytes.md) + +## RouteConfigOptionsBody.maxBytes property + +Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory. + +Default value: The one set in the kibana.yml config file under the parameter `server.maxPayloadBytes`. + +Signature: + +```typescript +maxBytes?: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.md new file mode 100644 index 0000000000000..6ef04de459fcf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) + +## RouteConfigOptionsBody interface + +Additional body options for a route + +Signature: + +```typescript +export interface RouteConfigOptionsBody +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [accepts](./kibana-plugin-server.routeconfigoptionsbody.accepts.md) | RouteContentType | RouteContentType[] | string | string[] | A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed above will not enable them to be parsed, and if parse is true, the request will result in an error response.Default value: allows parsing of the following mime types: \* application/json \* application/\*+json \* application/octet-stream \* application/x-www-form-urlencoded \* multipart/form-data \* text/\* | +| [maxBytes](./kibana-plugin-server.routeconfigoptionsbody.maxbytes.md) | number | Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory.Default value: The one set in the kibana.yml config file under the parameter server.maxPayloadBytes. | +| [output](./kibana-plugin-server.routeconfigoptionsbody.output.md) | typeof validBodyOutput[number] | The processed payload format. The value must be one of: \* 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw Buffer is returned. \* 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart payloads are a synthetic interface created on top of the entire multipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the multipart payload in the handler using a streaming parser (e.g. pez).Default value: 'data', unless no validation.body is provided in the route definition. In that case the default is 'stream' to alleviate memory pressure. | +| [parse](./kibana-plugin-server.routeconfigoptionsbody.parse.md) | boolean | 'gunzip' | Determines if the incoming payload is processed or presented raw. Available values: \* true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. \* false - the raw payload is returned unmodified. \* 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded.Default value: true, unless no validation.body is provided in the route definition. In that case the default is false to alleviate memory pressure. | + diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.output.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.output.md new file mode 100644 index 0000000000000..b84bc709df3ec --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.output.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) > [output](./kibana-plugin-server.routeconfigoptionsbody.output.md) + +## RouteConfigOptionsBody.output property + +The processed payload format. The value must be one of: \* 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw Buffer is returned. \* 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart payloads are a synthetic interface created on top of the entire multipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the multipart payload in the handler using a streaming parser (e.g. pez). + +Default value: 'data', unless no validation.body is provided in the route definition. In that case the default is 'stream' to alleviate memory pressure. + +Signature: + +```typescript +output?: typeof validBodyOutput[number]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.parse.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.parse.md new file mode 100644 index 0000000000000..d395f67c69669 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptionsbody.parse.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) > [parse](./kibana-plugin-server.routeconfigoptionsbody.parse.md) + +## RouteConfigOptionsBody.parse property + +Determines if the incoming payload is processed or presented raw. Available values: \* true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. \* false - the raw payload is returned unmodified. \* 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded. + +Default value: true, unless no validation.body is provided in the route definition. In that case the default is false to alleviate memory pressure. + +Signature: + +```typescript +parse?: boolean | 'gunzip'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routecontenttype.md b/docs/development/core/server/kibana-plugin-server.routecontenttype.md new file mode 100644 index 0000000000000..010388c7b8f17 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routecontenttype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteContentType](./kibana-plugin-server.routecontenttype.md) + +## RouteContentType type + +The set of supported parseable Content-Types + +Signature: + +```typescript +export declare type RouteContentType = 'application/json' | 'application/*+json' | 'application/octet-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routemethod.md b/docs/development/core/server/kibana-plugin-server.routemethod.md index dd1a050708bb3..4f83344f842b3 100644 --- a/docs/development/core/server/kibana-plugin-server.routemethod.md +++ b/docs/development/core/server/kibana-plugin-server.routemethod.md @@ -9,5 +9,5 @@ The set of common HTTP methods supported by Kibana routing. Signature: ```typescript -export declare type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +export declare type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeregistrar.md b/docs/development/core/server/kibana-plugin-server.routeregistrar.md index 535927dc73743..0f5f49636fdd5 100644 --- a/docs/development/core/server/kibana-plugin-server.routeregistrar.md +++ b/docs/development/core/server/kibana-plugin-server.routeregistrar.md @@ -4,10 +4,10 @@ ## RouteRegistrar type -Handler to declare a route. +Route handler common definition Signature: ```typescript -export declare type RouteRegistrar =

    (route: RouteConfig, handler: RequestHandler) => void; +export declare type RouteRegistrar =

    | Type>(route: RouteConfig, handler: RequestHandler) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeschemas.body.md b/docs/development/core/server/kibana-plugin-server.routeschemas.body.md new file mode 100644 index 0000000000000..78a9d25c25d9d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeschemas.body.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteSchemas](./kibana-plugin-server.routeschemas.md) > [body](./kibana-plugin-server.routeschemas.body.md) + +## RouteSchemas.body property + +Signature: + +```typescript +body?: B; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeschemas.md b/docs/development/core/server/kibana-plugin-server.routeschemas.md new file mode 100644 index 0000000000000..77b980551a8ff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeschemas.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteSchemas](./kibana-plugin-server.routeschemas.md) + +## RouteSchemas interface + +RouteSchemas contains the schemas for validating the different parts of a request. + +Signature: + +```typescript +export interface RouteSchemas

    | Type> +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-server.routeschemas.body.md) | B | | +| [params](./kibana-plugin-server.routeschemas.params.md) | P | | +| [query](./kibana-plugin-server.routeschemas.query.md) | Q | | + diff --git a/docs/development/core/server/kibana-plugin-server.routeschemas.params.md b/docs/development/core/server/kibana-plugin-server.routeschemas.params.md new file mode 100644 index 0000000000000..3dbf9fed94dc0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeschemas.params.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteSchemas](./kibana-plugin-server.routeschemas.md) > [params](./kibana-plugin-server.routeschemas.params.md) + +## RouteSchemas.params property + +Signature: + +```typescript +params?: P; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeschemas.query.md b/docs/development/core/server/kibana-plugin-server.routeschemas.query.md new file mode 100644 index 0000000000000..5be5830cb4bc8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeschemas.query.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteSchemas](./kibana-plugin-server.routeschemas.md) > [query](./kibana-plugin-server.routeschemas.query.md) + +## RouteSchemas.query property + +Signature: + +```typescript +query?: Q; +``` diff --git a/docs/development/core/server/kibana-plugin-server.validbodyoutput.md b/docs/development/core/server/kibana-plugin-server.validbodyoutput.md new file mode 100644 index 0000000000000..ea866abf887fb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.validbodyoutput.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [validBodyOutput](./kibana-plugin-server.validbodyoutput.md) + +## validBodyOutput variable + +The set of valid body.output + +Signature: + +```typescript +validBodyOutput: readonly ["data", "stream"] +``` diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md index 8ba2c43b5e1fe..fd62f1b3c03b2 100644 --- a/packages/kbn-config-schema/README.md +++ b/packages/kbn-config-schema/README.md @@ -12,6 +12,8 @@ Kibana configuration entries providing developers with a fully typed model of th - [`schema.number()`](#schemanumber) - [`schema.boolean()`](#schemaboolean) - [`schema.literal()`](#schemaliteral) + - [`schema.buffer()`](#schemabuffer) + - [`schema.stream()`](#schemastream) - [Composite types](#composite-types) - [`schema.arrayOf()`](#schemaarrayof) - [`schema.object()`](#schemaobject) @@ -173,6 +175,36 @@ const valueSchema = [ ]; ``` +#### `schema.buffer()` + +Validates input data as a NodeJS `Buffer`. + +__Output type:__ `Buffer` + +__Options:__ + * `defaultValue: TBuffer | Reference | (() => TBuffer)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TBuffer) => Buffer | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.buffer({ defaultValue: Buffer.from('Hi, there!') }); +``` + +#### `schema.stream()` + +Validates input data as a NodeJS `stream`. + +__Output type:__ `Stream`, `Readable` or `Writtable` + +__Options:__ + * `defaultValue: TStream | Reference | (() => TStream)` - defines a default value, see [Default values](#default-values) section for more details. + * `validate: (value: TStream) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details. + +__Usage:__ +```typescript +const valueSchema = schema.stream({ defaultValue: new Stream() }); +``` + ### Composite types #### `schema.arrayOf()` diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index 210b044421e7e..56b3096433c24 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -18,6 +18,7 @@ */ import { Duration } from 'moment'; +import { Stream } from 'stream'; import { ByteSizeValue } from './byte_size_value'; import { ContextReference, Reference, SiblingReference } from './references'; @@ -26,6 +27,7 @@ import { ArrayOptions, ArrayType, BooleanType, + BufferType, ByteSizeOptions, ByteSizeType, ConditionalType, @@ -52,6 +54,7 @@ import { UnionType, URIOptions, URIType, + StreamType, } from './types'; export { ObjectType, TypeOf, Type }; @@ -65,6 +68,14 @@ function boolean(options?: TypeOptions): Type { return new BooleanType(options); } +function buffer(options?: TypeOptions): Type { + return new BufferType(options); +} + +function stream(options?: TypeOptions): Type { + return new StreamType(options); +} + function string(options?: StringOptions): Type { return new StringType(options); } @@ -188,6 +199,7 @@ export const schema = { any, arrayOf, boolean, + buffer, byteSize, conditional, contextRef, @@ -201,6 +213,7 @@ export const schema = { object, oneOf, recordOf, + stream, siblingRef, string, uri, diff --git a/packages/kbn-config-schema/src/internals/index.ts b/packages/kbn-config-schema/src/internals/index.ts index e5a5b446de4f5..4d5091eaa09b1 100644 --- a/packages/kbn-config-schema/src/internals/index.ts +++ b/packages/kbn-config-schema/src/internals/index.ts @@ -29,6 +29,7 @@ import { } from 'joi'; import { isPlainObject } from 'lodash'; import { isDuration } from 'moment'; +import { Stream } from 'stream'; import { ByteSizeValue, ensureByteSizeValue } from '../byte_size_value'; import { ensureDuration } from '../duration'; @@ -89,6 +90,33 @@ export const internals = Joi.extend([ }, rules: [anyCustomRule], }, + { + name: 'binary', + + base: Joi.binary(), + coerce(value: any, state: State, options: ValidationOptions) { + // If value isn't defined, let Joi handle default value if it's defined. + if (value !== undefined && !(typeof value === 'object' && Buffer.isBuffer(value))) { + return this.createError('binary.base', { value }, state, options); + } + + return value; + }, + rules: [anyCustomRule], + }, + { + name: 'stream', + + pre(value: any, state: State, options: ValidationOptions) { + // If value isn't defined, let Joi handle default value if it's defined. + if (value instanceof Stream) { + return value as any; + } + + return this.createError('stream.base', { value }, state, options); + }, + rules: [anyCustomRule], + }, { name: 'string', diff --git a/packages/kbn-config-schema/src/types/__snapshots__/buffer_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/buffer_type.test.ts.snap new file mode 100644 index 0000000000000..96a7ab34dac26 --- /dev/null +++ b/packages/kbn-config-schema/src/types/__snapshots__/buffer_type.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [Buffer] but got [undefined]"`; + +exports[`is required by default 1`] = `"expected value of type [Buffer] but got [undefined]"`; + +exports[`returns error when not a buffer 1`] = `"expected value of type [Buffer] but got [number]"`; + +exports[`returns error when not a buffer 2`] = `"expected value of type [Buffer] but got [Array]"`; + +exports[`returns error when not a buffer 3`] = `"expected value of type [Buffer] but got [string]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/stream_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/stream_type.test.ts.snap new file mode 100644 index 0000000000000..e813b4f68a09e --- /dev/null +++ b/packages/kbn-config-schema/src/types/__snapshots__/stream_type.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [Stream] but got [undefined]"`; + +exports[`is required by default 1`] = `"expected value of type [Buffer] but got [undefined]"`; + +exports[`returns error when not a stream 1`] = `"expected value of type [Stream] but got [number]"`; + +exports[`returns error when not a stream 2`] = `"expected value of type [Stream] but got [Array]"`; + +exports[`returns error when not a stream 3`] = `"expected value of type [Stream] but got [string]"`; diff --git a/packages/kbn-config-schema/src/types/buffer_type.test.ts b/packages/kbn-config-schema/src/types/buffer_type.test.ts new file mode 100644 index 0000000000000..63d59296aec84 --- /dev/null +++ b/packages/kbn-config-schema/src/types/buffer_type.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '..'; + +test('returns value by default', () => { + const value = Buffer.from('Hi!'); + expect(schema.buffer().validate(value)).toStrictEqual(value); +}); + +test('is required by default', () => { + expect(() => schema.buffer().validate(undefined)).toThrowErrorMatchingSnapshot(); +}); + +test('includes namespace in failure', () => { + expect(() => + schema.buffer().validate(undefined, {}, 'foo-namespace') + ).toThrowErrorMatchingSnapshot(); +}); + +describe('#defaultValue', () => { + test('returns default when undefined', () => { + const value = Buffer.from('Hi!'); + expect(schema.buffer({ defaultValue: value }).validate(undefined)).toStrictEqual(value); + }); + + test('returns value when specified', () => { + const value = Buffer.from('Hi!'); + expect(schema.buffer({ defaultValue: Buffer.from('Bye!') }).validate(value)).toStrictEqual( + value + ); + }); +}); + +test('returns error when not a buffer', () => { + expect(() => schema.buffer().validate(123)).toThrowErrorMatchingSnapshot(); + + expect(() => schema.buffer().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + + expect(() => schema.buffer().validate('abc')).toThrowErrorMatchingSnapshot(); +}); diff --git a/packages/kbn-config-schema/src/types/buffer_type.ts b/packages/kbn-config-schema/src/types/buffer_type.ts new file mode 100644 index 0000000000000..194163e5096f0 --- /dev/null +++ b/packages/kbn-config-schema/src/types/buffer_type.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import typeDetect from 'type-detect'; +import { internals } from '../internals'; +import { Type, TypeOptions } from './type'; + +export class BufferType extends Type { + constructor(options?: TypeOptions) { + super(internals.binary(), options); + } + + protected handleError(type: string, { value }: Record) { + if (type === 'any.required' || type === 'binary.base') { + return `expected value of type [Buffer] but got [${typeDetect(value)}]`; + } + } +} diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index cfa8cc4b7553d..9db79b8bf9e00 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -21,6 +21,7 @@ export { Type, TypeOptions } from './type'; export { AnyType } from './any_type'; export { ArrayOptions, ArrayType } from './array_type'; export { BooleanType } from './boolean_type'; +export { BufferType } from './buffer_type'; export { ByteSizeOptions, ByteSizeType } from './byte_size_type'; export { ConditionalType, ConditionalTypeValue } from './conditional_type'; export { DurationOptions, DurationType } from './duration_type'; @@ -30,6 +31,7 @@ export { MapOfOptions, MapOfType } from './map_type'; export { NumberOptions, NumberType } from './number_type'; export { ObjectType, ObjectTypeOptions, Props, TypeOf } from './object_type'; export { RecordOfOptions, RecordOfType } from './record_type'; +export { StreamType } from './stream_type'; export { StringOptions, StringType } from './string_type'; export { UnionType } from './union_type'; export { URIOptions, URIType } from './uri_type'; diff --git a/packages/kbn-config-schema/src/types/stream_type.test.ts b/packages/kbn-config-schema/src/types/stream_type.test.ts new file mode 100644 index 0000000000000..011fa6373df33 --- /dev/null +++ b/packages/kbn-config-schema/src/types/stream_type.test.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '..'; +import { Stream, Readable, Writable, PassThrough } from 'stream'; + +test('returns value by default', () => { + const value = new Stream(); + expect(schema.stream().validate(value)).toStrictEqual(value); +}); + +test('Readable is valid', () => { + const value = new Readable(); + expect(schema.stream().validate(value)).toStrictEqual(value); +}); + +test('Writable is valid', () => { + const value = new Writable(); + expect(schema.stream().validate(value)).toStrictEqual(value); +}); + +test('Passthrough is valid', () => { + const value = new PassThrough(); + expect(schema.stream().validate(value)).toStrictEqual(value); +}); + +test('is required by default', () => { + expect(() => schema.buffer().validate(undefined)).toThrowErrorMatchingSnapshot(); +}); + +test('includes namespace in failure', () => { + expect(() => + schema.stream().validate(undefined, {}, 'foo-namespace') + ).toThrowErrorMatchingSnapshot(); +}); + +describe('#defaultValue', () => { + test('returns default when undefined', () => { + const value = new Stream(); + expect(schema.stream({ defaultValue: value }).validate(undefined)).toStrictEqual(value); + }); + + test('returns value when specified', () => { + const value = new Stream(); + expect(schema.stream({ defaultValue: new PassThrough() }).validate(value)).toStrictEqual(value); + }); +}); + +test('returns error when not a stream', () => { + expect(() => schema.stream().validate(123)).toThrowErrorMatchingSnapshot(); + + expect(() => schema.stream().validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + + expect(() => schema.stream().validate('abc')).toThrowErrorMatchingSnapshot(); +}); diff --git a/packages/kbn-config-schema/src/types/stream_type.ts b/packages/kbn-config-schema/src/types/stream_type.ts new file mode 100644 index 0000000000000..db1559f537490 --- /dev/null +++ b/packages/kbn-config-schema/src/types/stream_type.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import typeDetect from 'type-detect'; +import { Stream } from 'stream'; +import { internals } from '../internals'; +import { Type, TypeOptions } from './type'; + +export class StreamType extends Type { + constructor(options?: TypeOptions) { + super(internals.stream(), options); + } + + protected handleError(type: string, { value }: Record) { + if (type === 'any.required' || type === 'stream.base') { + return `expected value of type [Stream] but got [${typeDetect(value)}]`; + } + } +} diff --git a/packages/kbn-config-schema/types/joi.d.ts b/packages/kbn-config-schema/types/joi.d.ts index 5c7e42d0d6f5f..770314faa8ebd 100644 --- a/packages/kbn-config-schema/types/joi.d.ts +++ b/packages/kbn-config-schema/types/joi.d.ts @@ -38,6 +38,7 @@ declare module 'joi' { duration: () => AnySchema; map: () => MapSchema; record: () => RecordSchema; + stream: () => AnySchema; }; interface AnySchema { diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 0ac2f59525c32..8469a1d23a44b 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -54,7 +54,7 @@ function createKibanaRequestMock({ }: RequestFixtureOptions = {}) { const queryString = querystring.stringify(query); return KibanaRequest.from( - { + createRawRequestMock({ headers, params, query, @@ -71,13 +71,13 @@ function createKibanaRequestMock({ raw: { req: { socket }, }, - } as any, + }), { params: schema.object({}, { allowUnknowns: true }), body: schema.object({}, { allowUnknowns: true }), query: schema.object({}, { allowUnknowns: true }), } - ); + ) as KibanaRequest, Readonly<{}>, Readonly<{}>>; } type DeepPartial = T extends any[] diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index ceecfcfea1449..df47ffdc1176b 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -30,6 +30,7 @@ import { HttpConfig } from './http_config'; import { Router } from './router'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { HttpServer } from './http_server'; +import { Readable } from 'stream'; const cookieOptions = { name: 'sid', @@ -577,6 +578,157 @@ test('exposes route details of incoming request to a route handler', async () => }); }); +test('exposes route details of incoming request to a route handler (POST + payload options)', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router('', logger, enhanceWithContext); + router.post( + { + path: '/', + validate: { body: schema.object({ test: schema.number() }) }, + options: { body: { accepts: 'application/json' } }, + }, + (context, req, res) => res.ok({ body: req.route }) + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .post('/') + .send({ test: 1 }) + .expect(200, { + method: 'post', + path: '/', + options: { + authRequired: true, + tags: [], + body: { + parse: true, // hapi populates the default + maxBytes: 1024, // hapi populates the default + accepts: ['application/json'], + output: 'data', + }, + }, + }); +}); + +describe('body options', () => { + test('should reject the request because the Content-Type in the request is not valid', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router('', logger, enhanceWithContext); + router.post( + { + path: '/', + validate: { body: schema.object({ test: schema.number() }) }, + options: { body: { accepts: 'multipart/form-data' } }, // supertest sends 'application/json' + }, + (context, req, res) => res.ok({ body: req.route }) + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .post('/') + .send({ test: 1 }) + .expect(415, { + statusCode: 415, + error: 'Unsupported Media Type', + message: 'Unsupported Media Type', + }); + }); + + test('should reject the request because the payload is too large', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router('', logger, enhanceWithContext); + router.post( + { + path: '/', + validate: { body: schema.object({ test: schema.number() }) }, + options: { body: { maxBytes: 1 } }, + }, + (context, req, res) => res.ok({ body: req.route }) + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .post('/') + .send({ test: 1 }) + .expect(413, { + statusCode: 413, + error: 'Request Entity Too Large', + message: 'Payload content length greater than maximum allowed: 1', + }); + }); + + test('should not parse the content in the request', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router('', logger, enhanceWithContext); + router.post( + { + path: '/', + validate: { body: schema.buffer() }, + options: { body: { parse: false } }, + }, + (context, req, res) => { + try { + expect(req.body).toBeInstanceOf(Buffer); + expect(req.body.toString()).toBe(JSON.stringify({ test: 1 })); + return res.ok({ body: req.route.options.body }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .post('/') + .send({ test: 1 }) + .expect(200, { + parse: false, + maxBytes: 1024, // hapi populates the default + output: 'data', + }); + }); +}); + +test('should return a stream in the body', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router('', logger, enhanceWithContext); + router.put( + { + path: '/', + validate: { body: schema.stream() }, + options: { body: { output: 'stream' } }, + }, + (context, req, res) => { + try { + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); + } catch (err) { + return res.internalError({ body: err.message }); + } + } + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .put('/') + .send({ test: 1 }) + .expect(200, { + parse: true, + maxBytes: 1024, // hapi populates the default + output: 'stream', + }); +}); + describe('setup contract', () => { describe('#createSessionStorage', () => { it('creates session storage factory', async () => { diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index da97ab535516c..a587eed1f54ec 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -127,21 +127,26 @@ export class HttpServer { for (const router of this.registeredRouters) { for (const route of router.getRoutes()) { this.log.debug(`registering route handler for [${route.path}]`); - const { authRequired = true, tags } = route.options; // Hapi does not allow payload validation to be specified for 'head' or 'get' requests const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true }; + const { authRequired = true, tags, body = {} } = route.options; + const { accepts: allow, maxBytes, output, parse } = body; this.server.route({ handler: route.handler, method: route.method, path: route.path, options: { - auth: authRequired ? undefined : false, + // Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }` + auth: authRequired === true ? undefined : false, tags: tags ? Array.from(tags) : undefined, // TODO: This 'validate' section can be removed once the legacy platform is completely removed. // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default // validation applied in ./http_tools#getServerOptions // (All NP routes are already required to specify their own validation in order to access the payload) validate, + payload: [allow, maxBytes, output, parse].some(v => typeof v !== 'undefined') + ? { allow, maxBytes, output, parse } + : undefined, }, }); } diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index e9a2571382edc..6dab120b20e50 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -43,6 +43,7 @@ const createRouterMock = (): jest.Mocked => ({ get: jest.fn(), post: jest.fn(), put: jest.fn(), + patch: jest.fn(), delete: jest.fn(), getRoutes: jest.fn(), handleLegacyErrors: jest.fn().mockImplementation(handler => handler), diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index bed76201bb4f9..f9a3a91ec18ad 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -30,6 +30,7 @@ export { ErrorHttpResponseOptions, KibanaRequest, KibanaRequestRoute, + KibanaRequestRouteOptions, IKibanaResponse, KnownHeaders, LegacyRequest, @@ -44,8 +45,12 @@ export { RouteConfig, IRouter, RouteMethod, - RouteConfigOptions, RouteRegistrar, + RouteConfigOptions, + RouteSchemas, + RouteConfigOptionsBody, + RouteContentType, + validBodyOutput, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts index 706a9fe3b8887..c4b4d3840d1b9 100644 --- a/src/core/server/http/router/error_wrapper.ts +++ b/src/core/server/http/router/error_wrapper.ts @@ -23,13 +23,14 @@ import { KibanaRequest } from './request'; import { KibanaResponseFactory } from './response'; import { RequestHandler } from './router'; import { RequestHandlerContext } from '../../../server'; +import { RouteMethod } from './route'; export const wrapErrors =

    ( - handler: RequestHandler -): RequestHandler => { + handler: RequestHandler +): RequestHandler => { return async ( context: RequestHandlerContext, - request: KibanaRequest, TypeOf, TypeOf>, + request: KibanaRequest, TypeOf, TypeOf, RouteMethod>, response: KibanaResponseFactory ) => { try { diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index f07ad3cfe85c0..35bfb3ba9c33a 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -22,11 +22,20 @@ export { Router, RequestHandler, IRouter, RouteRegistrar } from './router'; export { KibanaRequest, KibanaRequestRoute, + KibanaRequestRouteOptions, isRealRequest, LegacyRequest, ensureRawRequest, } from './request'; -export { RouteMethod, RouteConfig, RouteConfigOptions } from './route'; +export { + RouteMethod, + RouteConfig, + RouteConfigOptions, + RouteSchemas, + RouteContentType, + RouteConfigOptionsBody, + validBodyOutput, +} from './route'; export { HapiResponseAdapter } from './response_adapter'; export { CustomHttpResponseOptions, diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 5d3b70ba27eee..b132899910569 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -20,23 +20,32 @@ import { Url } from 'url'; import { Request } from 'hapi'; -import { ObjectType, TypeOf } from '@kbn/config-schema'; +import { ObjectType, Type, TypeOf } from '@kbn/config-schema'; +import { Stream } from 'stream'; import { deepFreeze, RecursiveReadonly } from '../../../utils'; import { Headers } from './headers'; -import { RouteMethod, RouteSchemas, RouteConfigOptions } from './route'; +import { RouteMethod, RouteSchemas, RouteConfigOptions, validBodyOutput } from './route'; import { KibanaSocket, IKibanaSocket } from './socket'; const requestSymbol = Symbol('request'); +/** + * Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. + * @public + */ +export type KibanaRequestRouteOptions = Method extends 'get' | 'options' + ? Required, 'body'>> + : Required>; + /** * Request specific route information exposed to a handler. * @public * */ -export interface KibanaRequestRoute { +export interface KibanaRequestRoute { path: string; - method: RouteMethod | 'patch' | 'options'; - options: Required; + method: Method; + options: KibanaRequestRouteOptions; } /** @@ -50,17 +59,22 @@ export interface LegacyRequest extends Request {} // eslint-disable-line @typesc * Kibana specific abstraction for an incoming request. * @public */ -export class KibanaRequest { +export class KibanaRequest< + Params = unknown, + Query = unknown, + Body = unknown, + Method extends RouteMethod = any +> { /** * Factory for creating requests. Validates the request before creating an * instance of a KibanaRequest. * @internal */ - public static from

    ( - req: Request, - routeSchemas?: RouteSchemas, - withoutSecretHeaders: boolean = true - ) { + public static from< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType | Type | Type + >(req: Request, routeSchemas?: RouteSchemas, withoutSecretHeaders: boolean = true) { const requestParts = KibanaRequest.validate(req, routeSchemas); return new KibanaRequest( req, @@ -77,7 +91,11 @@ export class KibanaRequest { * received in the route handler. * @internal */ - private static validate

    ( + private static validate< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType | Type | Type + >( req: Request, routeSchemas: RouteSchemas | undefined ): { @@ -113,7 +131,7 @@ export class KibanaRequest { /** a WHATWG URL standard object. */ public readonly url: Url; /** matched route details */ - public readonly route: RecursiveReadonly; + public readonly route: RecursiveReadonly>; /** * Readonly copy of incoming request headers. * @remarks @@ -148,15 +166,28 @@ export class KibanaRequest { this.socket = new KibanaSocket(request.raw.req.socket); } - private getRouteInfo() { + private getRouteInfo(): KibanaRequestRoute { const request = this[requestSymbol]; + const method = request.method as Method; + const { parse, maxBytes, allow, output } = request.route.settings.payload || {}; + + const options = ({ + authRequired: request.route.settings.auth !== false, + tags: request.route.settings.tags || [], + body: ['get', 'options'].includes(method) + ? undefined + : { + parse, + maxBytes, + accepts: allow, + output: output as typeof validBodyOutput[number], // We do not support all the HAPI-supported outputs and TS complains + }, + } as unknown) as KibanaRequestRouteOptions; // TS does not understand this is OK so I'm enforced to do this enforced casting + return { path: request.path, - method: request.method, - options: { - authRequired: request.route.settings.auth !== false, - tags: request.route.settings.tags || [], - }, + method, + options, }; } } diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index bffa23551dd52..129cf4c922ffd 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -17,18 +17,89 @@ * under the License. */ -import { ObjectType } from '@kbn/config-schema'; +import { ObjectType, Type } from '@kbn/config-schema'; +import { Stream } from 'stream'; + /** * The set of common HTTP methods supported by Kibana routing. * @public */ -export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; + +/** + * The set of valid body.output + * @public + */ +export const validBodyOutput = ['data', 'stream'] as const; + +/** + * The set of supported parseable Content-Types + * @public + */ +export type RouteContentType = + | 'application/json' + | 'application/*+json' + | 'application/octet-stream' + | 'application/x-www-form-urlencoded' + | 'multipart/form-data' + | 'text/*'; + +/** + * Additional body options for a route + * @public + */ +export interface RouteConfigOptionsBody { + /** + * A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed + * above will not enable them to be parsed, and if parse is true, the request will result in an error response. + * + * Default value: allows parsing of the following mime types: + * * application/json + * * application/*+json + * * application/octet-stream + * * application/x-www-form-urlencoded + * * multipart/form-data + * * text/* + */ + accepts?: RouteContentType | RouteContentType[] | string | string[]; + + /** + * Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory. + * + * Default value: The one set in the kibana.yml config file under the parameter `server.maxPayloadBytes`. + */ + maxBytes?: number; + + /** + * The processed payload format. The value must be one of: + * * 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw + * Buffer is returned. + * * 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files + * are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart + * payloads are a synthetic interface created on top of the entire multipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the + * multipart payload in the handler using a streaming parser (e.g. pez). + * + * Default value: 'data', unless no validation.body is provided in the route definition. In that case the default is 'stream' to alleviate memory pressure. + */ + output?: typeof validBodyOutput[number]; + + /** + * Determines if the incoming payload is processed or presented raw. Available values: + * * true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the + * format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. + * * false - the raw payload is returned unmodified. + * * 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded. + * + * Default value: true, unless no validation.body is provided in the route definition. In that case the default is false to alleviate memory pressure. + */ + parse?: boolean | 'gunzip'; +} /** * Additional route options. * @public */ -export interface RouteConfigOptions { +export interface RouteConfigOptions { /** * A flag shows that authentication for a route: * `enabled` when true @@ -42,13 +113,23 @@ export interface RouteConfigOptions { * Additional metadata tag strings to attach to the route. */ tags?: readonly string[]; + + /** + * Additional body options {@link RouteConfigOptionsBody}. + */ + body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; } /** * Route specific configuration. * @public */ -export interface RouteConfig

    { +export interface RouteConfig< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType | Type | Type, + Method extends RouteMethod +> { /** * The endpoint _within_ the router path to register the route. * @@ -125,7 +206,7 @@ export interface RouteConfig

    ; } /** @@ -133,7 +214,11 @@ export interface RouteConfig

    { +export interface RouteSchemas< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType | Type | Type +> { params?: P; query?: Q; body?: B; diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts index 9fdf7297ed775..f5469a95b5106 100644 --- a/src/core/server/http/router/router.test.ts +++ b/src/core/server/http/router/router.test.ts @@ -19,6 +19,7 @@ import { Router } from './router'; import { loggingServiceMock } from '../../logging/logging_service.mock'; +import { schema } from '@kbn/config-schema'; const logger = loggingServiceMock.create().get(); const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); @@ -45,5 +46,46 @@ describe('Router', () => { `"Expected a valid schema declared with '@kbn/config-schema' package at key: [params]."` ); }); + + it('throws if options.body.output is not a valid value', () => { + const router = new Router('', logger, enhanceWithContext); + expect(() => + router.post( + // we use 'any' because TS already checks we cannot provide this body.output + { + path: '/', + options: { body: { output: 'file' } } as any, // We explicitly don't support 'file' + validate: { body: schema.object({}, { allowUnknowns: true }) }, + }, + (context, req, res) => res.ok({}) + ) + ).toThrowErrorMatchingInlineSnapshot( + `"[options.body.output: 'file'] in route POST / is not valid. Only 'data' or 'stream' are valid."` + ); + }); + + it('should default `output: "stream" and parse: false` when no body validation is required but not a GET', () => { + const router = new Router('', logger, enhanceWithContext); + router.post({ path: '/', validate: {} }, (context, req, res) => res.ok({})); + const [route] = router.getRoutes(); + expect(route.options).toEqual({ body: { output: 'stream', parse: false } }); + }); + + it('should NOT default `output: "stream" and parse: false` when the user has specified body options (he cares about it)', () => { + const router = new Router('', logger, enhanceWithContext); + router.post( + { path: '/', options: { body: { maxBytes: 1 } }, validate: {} }, + (context, req, res) => res.ok({}) + ); + const [route] = router.getRoutes(); + expect(route.options).toEqual({ body: { maxBytes: 1 } }); + }); + + it('should NOT default `output: "stream" and parse: false` when no body validation is required and GET', () => { + const router = new Router('', logger, enhanceWithContext); + router.get({ path: '/', validate: {} }, (context, req, res) => res.ok({})); + const [route] = router.getRoutes(); + expect(route.options).toEqual({}); + }); }); }); diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index a13eae51a19a6..3bed8fe4186ac 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -21,10 +21,17 @@ import { ObjectType, TypeOf, Type } from '@kbn/config-schema'; import { Request, ResponseObject, ResponseToolkit } from 'hapi'; import Boom from 'boom'; +import { Stream } from 'stream'; import { Logger } from '../../logging'; import { KibanaRequest } from './request'; import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from './response'; -import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route'; +import { + RouteConfig, + RouteConfigOptions, + RouteMethod, + RouteSchemas, + validBodyOutput, +} from './route'; import { HapiResponseAdapter } from './response_adapter'; import { RequestHandlerContext } from '../../../server'; import { wrapErrors } from './error_wrapper'; @@ -32,17 +39,22 @@ import { wrapErrors } from './error_wrapper'; interface RouterRoute { method: RouteMethod; path: string; - options: RouteConfigOptions; + options: RouteConfigOptions; handler: (req: Request, responseToolkit: ResponseToolkit) => Promise>; } /** - * Handler to declare a route. + * Route handler common definition + * * @public */ -export type RouteRegistrar =

    ( - route: RouteConfig, - handler: RequestHandler +export type RouteRegistrar = < + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType | Type | Type +>( + route: RouteConfig, + handler: RequestHandler ) => void; /** @@ -62,28 +74,35 @@ export interface IRouter { * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - get: RouteRegistrar; + get: RouteRegistrar<'get'>; /** * Register a route handler for `POST` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - post: RouteRegistrar; + post: RouteRegistrar<'post'>; /** * Register a route handler for `PUT` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - put: RouteRegistrar; + put: RouteRegistrar<'put'>; + + /** + * Register a route handler for `PATCH` request. + * @param route {@link RouteConfig} - a route configuration. + * @param handler {@link RequestHandler} - a function to call to respond to an incoming request + */ + patch: RouteRegistrar<'patch'>; /** * Register a route handler for `DELETE` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - delete: RouteRegistrar; + delete: RouteRegistrar<'delete'>; /** * Wrap a router handler to catch and converts legacy boom errors to proper custom errors. @@ -94,16 +113,19 @@ export interface IRouter { ) => RequestHandler; /** - * Returns all routes registered with the this router. + * Returns all routes registered with this router. * @returns List of registered routes. * @internal */ getRoutes: () => RouterRoute[]; } -export type ContextEnhancer

    = ( - handler: RequestHandler -) => RequestHandlerEnhanced; +export type ContextEnhancer< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType, + Method extends RouteMethod +> = (handler: RequestHandler) => RequestHandlerEnhanced; function getRouteFullPath(routerPath: string, routePath: string) { // If router's path ends with slash and route's path starts with slash, @@ -121,8 +143,8 @@ function getRouteFullPath(routerPath: string, routePath: string) { function routeSchemasFromRouteConfig< P extends ObjectType, Q extends ObjectType, - B extends ObjectType ->(route: RouteConfig, routeMethod: RouteMethod) { + B extends ObjectType | Type | Type +>(route: RouteConfig, routeMethod: RouteMethod) { // The type doesn't allow `validate` to be undefined, but it can still // happen when it's used from JavaScript. if (route.validate === undefined) { @@ -144,6 +166,49 @@ function routeSchemasFromRouteConfig< return route.validate ? route.validate : undefined; } +/** + * Create a valid options object with "sensible" defaults + adding some validation to the options fields + * + * @param method HTTP verb for these options + * @param routeConfig The route config definition + */ +function validOptions( + method: RouteMethod, + routeConfig: RouteConfig< + ObjectType, + ObjectType, + ObjectType | Type | Type, + typeof method + > +) { + const shouldNotHavePayload = ['head', 'get'].includes(method); + const { options = {}, validate } = routeConfig; + const shouldValidateBody = (validate && !!validate.body) || !!options.body; + + const { output } = options.body || {}; + if (typeof output === 'string' && !validBodyOutput.includes(output)) { + throw new Error( + `[options.body.output: '${output}'] in route ${method.toUpperCase()} ${ + routeConfig.path + } is not valid. Only '${validBodyOutput.join("' or '")}' are valid.` + ); + } + + const body = shouldNotHavePayload + ? undefined + : { + // If it's not a GET (requires payload) but no body validation is required (or no body options are specified), + // We assume the route does not care about the body => use the memory-cheapest approach (stream and no parsing) + output: !shouldValidateBody ? ('stream' as const) : undefined, + parse: !shouldValidateBody ? false : undefined, + + // User's settings should overwrite any of the "desired" values + ...options.body, + }; + + return { ...options, body }; +} + /** * @internal */ @@ -153,21 +218,21 @@ export class Router implements IRouter { public post: IRouter['post']; public delete: IRouter['delete']; public put: IRouter['put']; + public patch: IRouter['patch']; constructor( public readonly routerPath: string, private readonly log: Logger, - private readonly enhanceWithContext: ContextEnhancer + private readonly enhanceWithContext: ContextEnhancer ) { - const buildMethod = (method: RouteMethod) => < + const buildMethod = (method: Method) => < P extends ObjectType, Q extends ObjectType, - B extends ObjectType + B extends ObjectType | Type | Type >( - route: RouteConfig, - handler: RequestHandler + route: RouteConfig, + handler: RequestHandler ) => { - const { path, options = {} } = route; const routeSchemas = routeSchemasFromRouteConfig(route, method); this.routes.push({ @@ -179,8 +244,8 @@ export class Router implements IRouter { handler: this.enhanceWithContext(handler), }), method, - path: getRouteFullPath(this.routerPath, path), - options, + path: getRouteFullPath(this.routerPath, route.path), + options: validOptions(method, route), }); }; @@ -188,6 +253,7 @@ export class Router implements IRouter { this.post = buildMethod('post'); this.delete = buildMethod('delete'); this.put = buildMethod('put'); + this.patch = buildMethod('patch'); } public getRoutes() { @@ -200,7 +266,11 @@ export class Router implements IRouter { return wrapErrors(handler); } - private async handle

    ({ + private async handle< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType | Type | Type + >({ routeSchemas, request, responseToolkit, @@ -208,10 +278,10 @@ export class Router implements IRouter { }: { request: Request; responseToolkit: ResponseToolkit; - handler: RequestHandlerEnhanced; + handler: RequestHandlerEnhanced; routeSchemas?: RouteSchemas; }) { - let kibanaRequest: KibanaRequest, TypeOf, TypeOf>; + let kibanaRequest: KibanaRequest, TypeOf, TypeOf, typeof request.method>; const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); try { kibanaRequest = KibanaRequest.from(request, routeSchemas); @@ -236,8 +306,9 @@ type WithoutHeadArgument = T extends (first: any, ...rest: infer Params) => i type RequestHandlerEnhanced< P extends ObjectType, Q extends ObjectType, - B extends ObjectType -> = WithoutHeadArgument>; + B extends ObjectType | Type | Type, + Method extends RouteMethod +> = WithoutHeadArgument>; /** * A function executed when route path matched requested resource path. @@ -272,8 +343,13 @@ type RequestHandlerEnhanced< * ``` * @public */ -export type RequestHandler

    = ( +export type RequestHandler< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType | Type | Type, + Method extends RouteMethod = any +> = ( context: RequestHandlerContext, - request: KibanaRequest, TypeOf, TypeOf>, + request: KibanaRequest, TypeOf, TypeOf, Method>, response: KibanaResponseFactory ) => IKibanaResponse | Promise>; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index f792f6e604c15..a54ada233bbc9 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -94,6 +94,7 @@ export { IsAuthenticated, KibanaRequest, KibanaRequestRoute, + KibanaRequestRouteOptions, IKibanaResponse, LifecycleResponseFactory, KnownHeaders, @@ -113,9 +114,13 @@ export { KibanaResponseFactory, RouteConfig, IRouter, + RouteRegistrar, RouteMethod, RouteConfigOptions, - RouteRegistrar, + RouteSchemas, + RouteConfigOptionsBody, + RouteContentType, + validBodyOutput, SessionStorage, SessionStorageCookieOptions, SessionCookieValidationResult, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 411e5636069c1..25ca8ade77aca 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -449,11 +449,11 @@ export interface AuthToolkit { export class BasePath { // @internal constructor(serverBasePath?: string); - get: (request: KibanaRequest | LegacyRequest) => string; + get: (request: KibanaRequest | LegacyRequest) => string; prepend: (path: string) => string; remove: (path: string) => string; readonly serverBasePath: string; - set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; + set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; } // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts @@ -718,15 +718,16 @@ export interface IndexSettingsDeprecationInfo { // @public export interface IRouter { - delete: RouteRegistrar; - get: RouteRegistrar; + delete: RouteRegistrar<'delete'>; + get: RouteRegistrar<'get'>; // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts // // @internal getRoutes: () => RouterRoute[]; handleLegacyErrors:

    (handler: RequestHandler) => RequestHandler; - post: RouteRegistrar; - put: RouteRegistrar; + patch: RouteRegistrar<'patch'>; + post: RouteRegistrar<'post'>; + put: RouteRegistrar<'put'>; routerPath: string; } @@ -753,37 +754,38 @@ export interface IUiSettingsClient { } // @public -export class KibanaRequest { +export class KibanaRequest { // @internal (undocumented) protected readonly [requestSymbol]: Request; constructor(request: Request, params: Params, query: Query, body: Body, withoutSecretHeaders: boolean); // (undocumented) readonly body: Body; - // Warning: (ae-forgotten-export) The symbol "RouteSchemas" needs to be exported by the entry point index.d.ts - // // @internal - static from

    (req: Request, routeSchemas?: RouteSchemas, withoutSecretHeaders?: boolean): KibanaRequest; + static from

    | Type>(req: Request, routeSchemas?: RouteSchemas, withoutSecretHeaders?: boolean): KibanaRequest; readonly headers: Headers; // (undocumented) readonly params: Params; // (undocumented) readonly query: Query; - readonly route: RecursiveReadonly; + readonly route: RecursiveReadonly>; // (undocumented) readonly socket: IKibanaSocket; readonly url: Url; } // @public -export interface KibanaRequestRoute { +export interface KibanaRequestRoute { // (undocumented) - method: RouteMethod | 'patch' | 'options'; + method: Method; // (undocumented) - options: Required; + options: KibanaRequestRouteOptions; // (undocumented) path: string; } +// @public +export type KibanaRequestRouteOptions = Method extends 'get' | 'options' ? Required, 'body'>> : Required>; + // @public export type KibanaResponseFactory = typeof kibanaResponseFactory; @@ -1050,7 +1052,7 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; // @public -export type RequestHandler

    = (context: RequestHandlerContext, request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory) => IKibanaResponse | Promise>; +export type RequestHandler

    | Type, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest, TypeOf, TypeOf, Method>, response: KibanaResponseFactory) => IKibanaResponse | Promise>; // @public export interface RequestHandlerContext { @@ -1092,23 +1094,45 @@ export type ResponseHeaders = { }; // @public -export interface RouteConfig

    { - options?: RouteConfigOptions; +export interface RouteConfig

    | Type, Method extends RouteMethod> { + options?: RouteConfigOptions; path: string; validate: RouteSchemas | false; } // @public -export interface RouteConfigOptions { +export interface RouteConfigOptions { authRequired?: boolean; + body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; tags?: readonly string[]; } // @public -export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +export interface RouteConfigOptionsBody { + accepts?: RouteContentType | RouteContentType[] | string | string[]; + maxBytes?: number; + output?: typeof validBodyOutput[number]; + parse?: boolean | 'gunzip'; +} + +// @public +export type RouteContentType = 'application/json' | 'application/*+json' | 'application/octet-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*'; + +// @public +export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; // @public -export type RouteRegistrar =

    (route: RouteConfig, handler: RequestHandler) => void; +export type RouteRegistrar =

    | Type>(route: RouteConfig, handler: RequestHandler) => void; + +// @public +export interface RouteSchemas

    | Type> { + // (undocumented) + body?: B; + // (undocumented) + params?: P; + // (undocumented) + query?: Q; +} // @public (undocumented) export interface SavedObject { @@ -1696,6 +1720,9 @@ export interface UserProvidedValues { userValue?: T; } +// @public +export const validBodyOutput: readonly ["data", "stream"]; + // Warnings were encountered during analysis: // diff --git a/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts b/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts index 2e97b01d0d108..2bbd8b6ddfb62 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import * as t from 'io-ts'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; -import { KibanaResponseFactory } from 'src/core/server'; +import { KibanaResponseFactory, RouteRegistrar } from 'src/core/server'; import { APMConfig } from '../../../../../../plugins/apm/server'; import { ServerAPI, @@ -65,7 +65,7 @@ export function createApi() { body: bodyRt && 'props' in bodyRt ? t.exact(bodyRt) : fallbackBodyRt }; - router[routerMethod]( + (router[routerMethod] as RouteRegistrar)( { path, options, diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index cdef1826ddaa8..c8735f9f87f4a 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -41,8 +41,8 @@ describe('SAML authentication routes', () => { }); describe('Assertion consumer service endpoint', () => { - let routeHandler: RequestHandler; - let routeConfig: RouteConfig; + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; beforeEach(() => { const [acsRouteConfig, acsRouteHandler] = router.post.mock.calls.find( ([{ path }]) => path === '/api/security/saml/callback' From 439708a6f9e9fd70e773409bbd03e1e723906170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 28 Nov 2019 15:26:47 +0530 Subject: [PATCH 125/128] [Dependencies]: upgrade react to latest v16.12.0 (#51145) --- package.json | 28 +- packages/eslint-config-kibana/javascript.js | 2 +- packages/eslint-config-kibana/jest.js | 2 +- packages/eslint-config-kibana/typescript.js | 2 +- packages/kbn-i18n/package.json | 2 +- .../src/components/guide_nav/guide_nav.js | 2 +- .../components/guide_sandbox/guide_sandbox.js | 2 +- .../components/guide_section/guide_section.js | 2 +- .../doc_site/src/views/app_view.js | 2 +- packages/kbn-ui-framework/package.json | 6 +- .../public/components/editor/field_select.js | 2 +- .../kibana/public/discover/doc/doc.test.tsx | 10 - .../discover/doc/use_es_doc_search.test.tsx | 12 +- .../home/components/tutorial/tutorial.js | 2 +- .../components/indices_list/indices_list.js | 2 +- .../step_index_pattern/step_index_pattern.js | 2 +- .../create_index_pattern_wizard.js | 2 +- .../indexed_fields_table.js | 2 +- .../scripted_fields_table.js | 2 +- .../source_filters_table.js | 2 +- .../components/relationships/relationships.js | 4 +- .../sections/settings/advanced_settings.js | 2 +- .../settings/components/field/field.js | 2 +- .../public/components/telemetry_form.js | 2 +- .../public/components/aggs/calculation.js | 2 +- .../public/components/aggs/math.js | 2 +- .../public/components/aggs/percentile.js | 2 +- .../public/components/custom_color_picker.js | 9 +- .../public/components/panel_config/gauge.js | 2 +- .../public/components/panel_config/metric.js | 2 +- .../public/components/panel_config/table.js | 2 +- .../public/components/panel_config/top_n.js | 2 +- .../public/components/split.js | 2 +- .../components/vis_types/table/config.js | 2 +- .../public/visualizations/views/gauge.js | 2 +- .../public/visualizations/views/gauge_vis.js | 2 +- .../public/visualizations/views/metric.js | 2 +- .../exit_full_screen_button.tsx | 2 +- .../vislib_vis_legend.test.tsx | 2 +- .../index_pattern_select.tsx | 2 +- .../public/lib/panel/embeddable_panel.tsx | 2 +- .../eui_utils/public/eui_utils.test.tsx | 2 +- .../exit_full_screen_button.tsx | 2 +- .../table_list_view/table_list_view.tsx | 2 +- .../public/util/use_observable.test.tsx | 2 +- .../util/use_shallow_compare_effect.test.ts | 2 +- .../plugins/kbn_tp_run_pipeline/package.json | 4 +- .../kbn_tp_custom_visualizations/package.json | 2 +- .../kbn_tp_embeddable_explorer/package.json | 2 +- .../kbn_tp_sample_panel_action/package.json | 2 +- x-pack/dev-tools/jest/setup/setup_test.js | 1 + .../__test__/ServiceOverview.test.tsx | 13 +- .../__jest__/TransactionOverview.test.tsx | 17 +- .../__test__/KeyValueTable.test.tsx | 4 +- .../__test__/ManagedTable.test.js | 6 +- .../components/shared/ManagedTable/index.tsx | 2 +- .../__test__/ErrorMetadata.test.tsx | 5 +- .../__test__/SpanMetadata.test.tsx | 4 +- .../__test__/TransactionMetadata.test.tsx | 5 +- .../__test__/MetadataTable.test.tsx | 4 +- .../MetadataTable/__test__/Section.test.tsx | 3 +- .../__test__/TransactionActionMenu.test.tsx | 4 +- .../useDelayedVisibility/index.test.tsx | 14 +- .../hooks/useAvgDurationByBrowser.test.ts | 2 +- .../hooks/useFetcher.integration.test.tsx | 12 +- .../apm/public/hooks/useFetcher.test.tsx | 14 +- .../plugins/apm/public/utils/testHelpers.tsx | 3 +- .../public/components/inputs/code_editor.tsx | 2 +- .../public/components/inputs/input.tsx | 2 +- .../public/components/inputs/multi_input.tsx | 2 +- .../public/components/inputs/select.tsx | 2 +- .../public/pages/beat/details.tsx | 2 +- .../public/pages/beat/tags.tsx | 2 +- .../public/pages/tag/edit.tsx | 2 +- .../beats_management/public/router.tsx | 2 +- .../components/time_picker/time_picker.tsx | 2 +- .../__tests__/workpad_telemetry.test.tsx | 4 +- .../components/arg_form/arg_template_form.js | 2 +- .../components/enhance/stateful_prop.js | 2 +- .../components/fullscreen/fullscreen.js | 2 +- .../function_form_context_pending.js | 2 +- .../render_with_fn/render_with_fn.js | 2 +- .../canvas/public/components/router/router.js | 2 +- .../workpad_loader/workpad_loader.js | 2 +- .../components/__tests__/app.test.tsx | 8 +- .../public/app/app.js | 2 +- .../components/field_manager/field_editor.tsx | 5 +- .../components/settings/settings.test.tsx | 2 +- .../node_attrs_details/node_attrs_details.js | 2 +- .../helpers/home.helpers.ts | 1 - .../__jest__/client_integration/home.test.ts | 14 +- .../template_clone.test.tsx | 7 +- .../template_create.test.tsx | 16 +- .../client_integration/template_edit.test.tsx | 7 +- .../edit_settings_json/edit_settings_json.js | 2 +- .../detail_panel/show_json/show_json.js | 4 +- .../use_metrics_explorer_data.test.tsx | 2 +- .../use_metrics_explorer_options.test.tsx | 10 +- .../use_metric_explorer_state.test.tsx | 6 +- .../pages/link_to/use_host_ip_to_name.test.ts | 2 +- .../dimension_panel/dimension_panel.test.tsx | 22 +- .../dimension_panel/dimension_panel.tsx | 6 +- .../start_trial/start_trial.js | 2 +- .../maps/public/components/map_listing.js | 2 +- .../severity_cell/severity_cell.test.tsx | 4 +- .../job_selector/id_badges/id_badges.test.js | 31 +- .../job_selector_table.test.js | 60 +- .../new_selection_id_badges.test.js | 23 +- .../analytics_list/action_delete.test.tsx | 5 +- .../ml/public/application/util/test_utils.ts | 6 +- .../public/components/chart/chart_target.js | 2 +- .../chart/timeseries_visualization.js | 2 +- .../components/cluster_view.js | 2 +- .../public/components/renderers/setup_mode.js | 2 +- .../public/components/sparkline/index.js | 2 +- .../edit_role/components/edit_role_page.tsx | 2 +- .../components/charts/areachart.test.tsx | 6 +- .../public/components/charts/areachart.tsx | 28 +- .../components/charts/barchart.test.tsx | 6 +- .../public/components/charts/barchart.tsx | 26 +- .../__snapshots__/embedded_map.test.tsx.snap | 2 +- .../embeddables/embedded_map.test.tsx | 6 +- .../components/embeddables/embedded_map.tsx | 276 ++++----- .../index_patterns_missing_prompt.test.tsx | 4 +- .../index_patterns_missing_prompt.tsx | 10 +- .../point_tool_tip_content.test.tsx.snap | 2 +- .../line_tool_tip_content.test.tsx | 4 +- .../map_tool_tip/line_tool_tip_content.tsx | 75 +-- .../map_tool_tip/map_tool_tip.test.tsx | 6 +- .../embeddables/map_tool_tip/map_tool_tip.tsx | 230 +++---- .../point_tool_tip_content.test.tsx | 6 +- .../map_tool_tip/point_tool_tip_content.tsx | 62 +- .../map_tool_tip/tooltip_footer.test.tsx | 36 +- .../map_tool_tip/tooltip_footer.tsx | 79 +-- .../events_viewer/events_viewer.test.tsx | 11 +- .../components/events_viewer/index.test.tsx | 10 - .../components/fields_browser/index.test.tsx | 29 +- .../components/formatted_bytes/index.test.tsx | 12 +- .../components/formatted_bytes/index.tsx | 8 +- .../__snapshots__/markdown_hint.test.tsx.snap | 56 +- .../markdown/markdown_hint.test.tsx | 22 +- .../components/markdown/markdown_hint.tsx | 9 +- .../components/ml/entity_draggable.test.tsx | 14 +- .../public/components/ml/entity_draggable.tsx | 70 ++- .../ml/score/anomaly_score.test.tsx | 8 +- .../components/ml/score/anomaly_score.tsx | 76 +-- .../ml/score/anomaly_scores.test.tsx | 16 +- .../components/ml/score/anomaly_scores.tsx | 69 ++- .../ml/score/draggable_score.test.tsx | 8 +- .../components/ml/score/draggable_score.tsx | 80 +-- .../__snapshots__/jobs_table.test.tsx.snap | 2 +- .../filters/groups_filter_popover.test.tsx | 6 +- .../filters/groups_filter_popover.tsx | 111 ++-- .../filters/jobs_table_filters.test.tsx | 14 +- .../jobs_table/filters/jobs_table_filters.tsx | 112 ++-- .../ml_popover/jobs_table/job_switch.test.tsx | 10 +- .../ml_popover/jobs_table/job_switch.tsx | 58 +- .../ml_popover/jobs_table/jobs_table.test.tsx | 40 +- .../ml_popover/jobs_table/jobs_table.tsx | 8 +- .../jobs_table/showing_count.test.tsx | 4 +- .../ml_popover/jobs_table/showing_count.tsx | 8 +- .../components/ml_popover/ml_popover.test.tsx | 12 - .../ml_popover/popover_description.test.tsx | 4 +- .../ml_popover/popover_description.tsx | 8 +- .../ml_popover/upgrade_contents.test.tsx | 4 +- .../ml_popover/upgrade_contents.tsx | 90 +-- .../navigation/tab_navigation/index.test.tsx | 14 +- .../navigation/tab_navigation/index.tsx | 9 +- .../hosts/first_last_seen_host/index.test.tsx | 24 +- .../page/hosts/host_overview/index.test.tsx | 21 - .../page/hosts/kpi_hosts/index.test.tsx | 10 +- .../components/page/hosts/kpi_hosts/index.tsx | 65 +- .../public/components/tables/helpers.test.tsx | 10 +- .../siem/public/components/tables/helpers.tsx | 28 +- .../body/column_headers/header/index.test.tsx | 24 +- .../body/column_headers/header/index.tsx | 98 +-- .../body/column_headers/index.test.tsx | 10 +- .../timeline/body/column_headers/index.tsx | 237 ++++---- .../timeline/body/renderers/args.test.tsx | 21 +- .../timeline/body/renderers/args.tsx | 8 +- .../components/timeline/footer/index.test.tsx | 24 +- .../components/timeline/footer/index.tsx | 339 ++++++----- .../components/timeline/header/index.test.tsx | 8 +- .../components/timeline/header/index.tsx | 88 +-- .../components/timeline/timeline.test.tsx | 26 +- .../public/components/timeline/timeline.tsx | 270 ++++----- .../truncatable_text/index.test.tsx | 3 +- .../import_rule_modal/index.test.tsx | 8 +- .../components/import_rule_modal/index.tsx | 220 +++---- .../components/json_downloader/index.test.tsx | 4 +- .../components/json_downloader/index.tsx | 48 +- .../components/rule_switch/index.test.tsx | 9 +- .../rules/components/rule_switch/index.tsx | 49 +- .../siem/public/pages/hosts/hosts.test.tsx | 11 - .../pages/network/ip_details/index.test.tsx | 6 - .../public/pages/network/ip_details/index.tsx | 432 +++++++------- .../public/pages/network/network.test.tsx | 10 - .../helpers/home.helpers.ts | 3 - .../__jest__/client_integration/home.test.ts | 14 - .../client_integration/policy_add.test.ts | 3 - .../client_integration/policy_edit.test.ts | 3 - .../client_integration/repository_add.test.ts | 5 - .../repository_edit.test.ts | 2 - .../copy_to_space_flyout.test.tsx | 2 +- .../toast_notification_text.test.tsx | 4 +- .../overview/deprecation_logging_toggle.tsx | 2 +- .../__tests__/filter_popover.test.tsx | 2 +- .../helpers/watch_list.helpers.ts | 2 - .../helpers/watch_status.helpers.ts | 4 - .../watch_create_json.test.ts | 5 - .../watch_create_threshold.test.tsx | 14 - .../client_integration/watch_edit.test.ts | 4 - .../client_integration/watch_list.test.ts | 3 - .../client_integration/watch_status.test.ts | 2 - x-pack/package.json | 22 +- x-pack/test_utils/testbed/mount_component.tsx | 9 - yarn.lock | 564 ++++++++++++------ 217 files changed, 2673 insertions(+), 2554 deletions(-) diff --git a/package.json b/package.json index 45a376a291359..4b48731bd9a88 100644 --- a/package.json +++ b/package.json @@ -79,17 +79,16 @@ }, "resolutions": { "**/@types/node": "10.12.27", - "**/@types/react": "16.8.3", + "**/@types/react": "^16.9.13", "**/@types/hapi": "^17.0.18", "**/@types/angular": "^1.6.56", "**/typescript": "3.7.2", "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", "**/image-diff/gm/debug": "^2.6.9", - "**/deepmerge": "^4.2.2", - "**/react": "16.8.6", - "**/react-dom": "16.8.6", - "**/react-test-renderer": "16.8.6" + "**/react-dom": "^16.12.0", + "**/react-test-renderer": "^16.12.0", + "**/deepmerge": "^4.2.2" }, "workspaces": { "packages": [ @@ -216,15 +215,13 @@ "pug": "^2.0.3", "querystring-browser": "1.0.4", "raw-loader": "3.1.0", - "react": "^16.8.6", - "react-addons-shallow-compare": "15.6.2", + "react": "^16.12.0", "react-color": "^2.13.8", - "react-dom": "^16.8.6", + "react-dom": "^16.12.0", "react-grid-layout": "^0.16.2", - "react-hooks-testing-library": "^0.5.0", "react-input-range": "^1.3.0", "react-markdown": "^3.4.1", - "react-redux": "^5.1.1", + "react-redux": "^5.1.2", "react-router-dom": "^4.3.1", "react-sizeme": "^2.3.6", "reactcss": "1.2.3", @@ -287,6 +284,8 @@ "@microsoft/api-documenter": "7.4.3", "@microsoft/api-extractor": "7.4.2", "@percy/agent": "^0.11.0", + "@testing-library/react": "^9.3.2", + "@testing-library/react-hooks": "^3.2.1", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", "@types/babel__core": "^7.1.2", @@ -312,7 +311,7 @@ "@types/has-ansi": "^3.0.0", "@types/history": "^4.7.3", "@types/hoek": "^4.1.3", - "@types/jest": "^24.0.18", + "@types/jest": "24.0.19", "@types/joi": "^13.4.2", "@types/jquery": "^3.3.31", "@types/js-yaml": "^3.11.1", @@ -332,8 +331,8 @@ "@types/podium": "^1.0.0", "@types/prop-types": "^15.5.3", "@types/reach__router": "^1.2.6", - "@types/react": "^16.8.0", - "@types/react-dom": "^16.8.0", + "@types/react": "^16.9.11", + "@types/react-dom": "^16.9.4", "@types/react-redux": "^6.0.6", "@types/react-router-dom": "^4.3.1", "@types/react-virtualized": "^9.18.7", @@ -347,6 +346,8 @@ "@types/styled-components": "^4.4.0", "@types/supertest": "^2.0.5", "@types/supertest-as-promised": "^2.0.38", + "@types/testing-library__react": "^9.1.2", + "@types/testing-library__react-hooks": "^3.1.0", "@types/type-detect": "^4.0.1", "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", @@ -410,7 +411,6 @@ "istanbul-instrumenter-loader": "3.0.1", "jest": "^24.9.0", "jest-cli": "^24.9.0", - "jest-dom": "^3.5.0", "jest-raw-loader": "^1.0.1", "jimp": "0.8.4", "json5": "^1.0.1", diff --git a/packages/eslint-config-kibana/javascript.js b/packages/eslint-config-kibana/javascript.js index 0c79669c15e73..7afd3da3a7b93 100644 --- a/packages/eslint-config-kibana/javascript.js +++ b/packages/eslint-config-kibana/javascript.js @@ -40,7 +40,7 @@ module.exports = { rules: { 'block-scoped-var': 'error', - camelcase: [ 'error', { properties: 'never' } ], + camelcase: [ 'error', { properties: 'never', allow: ['^UNSAFE_'] } ], 'comma-dangle': 'off', 'comma-spacing': ['error', { before: false, after: true }], 'comma-style': [ 'error', 'last' ], diff --git a/packages/eslint-config-kibana/jest.js b/packages/eslint-config-kibana/jest.js index 2aa9627404a6c..d682277ff905a 100644 --- a/packages/eslint-config-kibana/jest.js +++ b/packages/eslint-config-kibana/jest.js @@ -16,7 +16,7 @@ module.exports = { rules: { 'jest/no-focused-tests': 'error', 'jest/no-identical-title': 'error', - 'import/order': 'off' + 'import/order': 'off', }, } ] diff --git a/packages/eslint-config-kibana/typescript.js b/packages/eslint-config-kibana/typescript.js index 8ffae5edc88eb..3337bfc8eb101 100644 --- a/packages/eslint-config-kibana/typescript.js +++ b/packages/eslint-config-kibana/typescript.js @@ -94,7 +94,7 @@ module.exports = { '@typescript-eslint/camelcase': ['error', { 'properties': 'never', 'ignoreDestructuring': true, - 'allow': ['^[A-Z0-9_]+$'] + 'allow': ['^[A-Z0-9_]+$', '^UNSAFE_'] }], '@typescript-eslint/class-name-casing': 'error', '@typescript-eslint/explicit-member-accessibility': ['error', diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 0146111941044..bbc5126da1dce 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -28,7 +28,7 @@ "intl-messageformat": "^2.2.0", "intl-relativeformat": "^2.1.0", "prop-types": "^15.6.2", - "react": "^16.8.6", + "react": "^16.12.0", "react-intl": "^2.8.0" } } diff --git a/packages/kbn-ui-framework/doc_site/src/components/guide_nav/guide_nav.js b/packages/kbn-ui-framework/doc_site/src/components/guide_nav/guide_nav.js index 53bc42ce33276..cb4654522e4e3 100644 --- a/packages/kbn-ui-framework/doc_site/src/components/guide_nav/guide_nav.js +++ b/packages/kbn-ui-framework/doc_site/src/components/guide_nav/guide_nav.js @@ -47,7 +47,7 @@ export class GuideNav extends Component { }); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const currentRoute = nextProps.routes[1]; const nextRoute = this.props.getNextRoute(currentRoute.name); const previousRoute = this.props.getPreviousRoute(currentRoute.name); diff --git a/packages/kbn-ui-framework/doc_site/src/components/guide_sandbox/guide_sandbox.js b/packages/kbn-ui-framework/doc_site/src/components/guide_sandbox/guide_sandbox.js index 6d4c9cddae0be..f5a71bfb1b296 100644 --- a/packages/kbn-ui-framework/doc_site/src/components/guide_sandbox/guide_sandbox.js +++ b/packages/kbn-ui-framework/doc_site/src/components/guide_sandbox/guide_sandbox.js @@ -49,7 +49,7 @@ function mapDispatchToProps(dispatch) { } class GuideSandboxComponent extends Component { - componentWillMount() { + UNSAFE_componentWillMount() { this.props.openSandbox(); } diff --git a/packages/kbn-ui-framework/doc_site/src/components/guide_section/guide_section.js b/packages/kbn-ui-framework/doc_site/src/components/guide_section/guide_section.js index 99a68e9575114..dbad59ffb3bd5 100644 --- a/packages/kbn-ui-framework/doc_site/src/components/guide_section/guide_section.js +++ b/packages/kbn-ui-framework/doc_site/src/components/guide_section/guide_section.js @@ -36,7 +36,7 @@ export class GuideSection extends Component { this.props.openCodeViewer(this.props.source, this.props.title); } - componentWillMount() { + UNSAFE_componentWillMount() { this.props.registerSection(this.getId(), this.props.title); } diff --git a/packages/kbn-ui-framework/doc_site/src/views/app_view.js b/packages/kbn-ui-framework/doc_site/src/views/app_view.js index 7a9d7a01b820a..fc14417564d72 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/app_view.js +++ b/packages/kbn-ui-framework/doc_site/src/views/app_view.js @@ -81,7 +81,7 @@ export class AppView extends Component { }); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { // Only force the chrome to be hidden if we're navigating from a non-sandbox to a sandbox. if (!this.props.isSandbox && nextProps.isSandbox) { this.setState({ diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index ee5424370fb06..b4d9d3dfee03f 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -19,7 +19,7 @@ "focus-trap-react": "^3.1.1", "lodash": "npm:@elastic/lodash@3.10.1-kibana3", "prop-types": "15.6.0", - "react": "^16.8.6", + "react": "^16.12.0", "react-ace": "^5.9.0", "react-color": "^2.13.8", "tabbable": "1.1.3", @@ -57,8 +57,8 @@ "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", - "react-dom": "^16.8.6", - "react-redux": "^5.0.6", + "react-dom": "^16.12.0", + "react-redux": "^5.1.2", "react-router": "^3.2.0", "react-router-redux": "^4.0.8", "redux": "3.7.2", diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js index 11c6f27af38c5..ce7d48a3f1376 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js @@ -49,7 +49,7 @@ class FieldSelectUi extends Component { this.loadFields(this.state.indexPatternId); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.indexPatternId !== nextProps.indexPatternId) { this.loadFields(nextProps.indexPatternId); } diff --git a/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx index b3efd23ea48d0..ee80f29c053dc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx @@ -45,16 +45,6 @@ beforeEach(() => { jest.clearAllMocks(); }); -// Suppress warnings about "act" until we use React 16.9 -/* eslint-disable no-console */ -const originalError = console.error; -beforeAll(() => { - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; -}); - export const waitForPromises = () => new Promise(resolve => setTimeout(resolve, 0)); /** diff --git a/src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.test.tsx index 083a5997ac5dd..2420eb2cd22bb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.test.tsx @@ -16,20 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { renderHook, act } from 'react-hooks-testing-library'; +import { renderHook, act } from '@testing-library/react-hooks'; import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_doc_search'; import { DocProps } from './doc'; -// Suppress warnings about "act" until we use React 16.9 -/* eslint-disable no-console */ -const originalError = console.error; -beforeAll(() => { - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; -}); - describe('Test of helper / hook', () => { test('buildSearchBody', () => { const indexPattern = { diff --git a/src/legacy/core_plugins/kibana/public/home/components/tutorial/tutorial.js b/src/legacy/core_plugins/kibana/public/home/components/tutorial/tutorial.js index 086fa5a059121..567bb3f83f22c 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/tutorial/tutorial.js +++ b/src/legacy/core_plugins/kibana/public/home/components/tutorial/tutorial.js @@ -67,7 +67,7 @@ class TutorialUi extends React.Component { } } - componentWillMount() { + UNSAFE_componentWillMount() { this._isMounted = true; } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.js index df90abce00e48..d0441ce3ceb8b 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.js @@ -61,7 +61,7 @@ export class IndicesList extends Component { this.pager = new Pager(props.indices.length, this.state.perPage, this.state.page); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.indices.length !== this.props.indices.length) { this.pager.setTotalItems(nextProps.indices.length); this.resetPageTo0(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js index 617262c13b034..4764b516dffec 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js @@ -75,7 +75,7 @@ export class StepIndexPattern extends Component { this.lastQuery = null; } - async componentWillMount() { + async UNSAFE_componentWillMount() { this.fetchExistingIndexPatterns(); if (this.state.query) { this.lastQuery = this.state.query; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js index a1f302dc87f6c..566277802174a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/create_index_pattern_wizard.js @@ -65,7 +65,7 @@ export class CreateIndexPatternWizard extends Component { }; } - async componentWillMount() { + async UNSAFE_componentWillMount() { this.fetchData(); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js index cb1c316c8af9b..b32c325e93653 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.js @@ -47,7 +47,7 @@ export class IndexedFieldsTable extends Component { }; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.fields !== this.props.fields) { this.setState({ fields: this.mapFields(nextProps.fields) diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js index d91f4836ee1d8..c7677955a8069 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/scripted_fields_table.js @@ -59,7 +59,7 @@ export class ScriptedFieldsTable extends Component { }; } - componentWillMount() { + UNSAFE_componentWillMount() { this.fetchFields(); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js index ba93485a1739a..1201e23c48e44 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/source_filters_table.js @@ -58,7 +58,7 @@ export class SourceFiltersTable extends Component { }; } - componentWillMount() { + UNSAFE_componentWillMount() { this.updateFilters(); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js index 278f7de38b19d..ee9fb70e31fb2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js @@ -58,11 +58,11 @@ export class Relationships extends Component { }; } - componentWillMount() { + UNSAFE_componentWillMount() { this.getRelationshipData(); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.savedObject.id !== this.props.savedObject.id) { this.getRelationshipData(); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.js index c2dabdffb2cd2..525f8495027c1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/advanced_settings.js @@ -81,7 +81,7 @@ export class AdvancedSettings extends Component { }, {}); } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const { config } = nextProps; const { query } = this.state; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js index a953d09906ed1..c790930e32aa0 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js @@ -76,7 +76,7 @@ export class Field extends PureComponent { this.changeImageForm = null; } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const { unsavedValue } = this.state; const { type, value, defVal } = nextProps.setting; const editableValue = this.getEditableValue(type, value, defVal); diff --git a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js index 6c6ace71af4d0..d4bbe1029b40d 100644 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js +++ b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js @@ -52,7 +52,7 @@ export class TelemetryForm extends Component { queryMatches: null, } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const { query } = nextProps; diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/calculation.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/calculation.js index 2eefeb35c26ff..76306ecf28994 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/calculation.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/calculation.js @@ -42,7 +42,7 @@ import { } from '@elastic/eui'; export class CalculationAgg extends Component { - componentWillMount() { + UNSAFE_componentWillMount() { if (!this.props.model.variables) { this.props.onChange( _.assign({}, this.props.model, { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/math.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/math.js index a73460798ebd8..c62012927f951 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/math.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/math.js @@ -42,7 +42,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; export class MathAgg extends Component { - componentWillMount() { + UNSAFE_componentWillMount() { if (!this.props.model.variables) { this.props.onChange( _.assign({}, this.props.model, { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile.js index ec16a0f2eb3ee..3ce5be5b6875a 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/aggs/percentile.js @@ -42,7 +42,7 @@ const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER]; export class PercentileAgg extends Component { // eslint-disable-line react/no-multi-comp - componentWillMount() { + UNSAFE_componentWillMount() { if (!this.props.model.percentiles) { this.props.onChange( _.assign({}, this.props.model, { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/custom_color_picker.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/custom_color_picker.js index 9fe237d9cb671..835628368efab 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/custom_color_picker.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/custom_color_picker.js @@ -18,7 +18,7 @@ */ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import { ColorWrap as colorWrap, @@ -32,18 +32,13 @@ import ChromePointer from 'react-color/lib/components/chrome/ChromePointer'; import ChromePointerCircle from 'react-color/lib/components/chrome/ChromePointerCircle'; import CompactColor from 'react-color/lib/components/compact/CompactColor'; import color from 'react-color/lib/helpers/color'; -import shallowCompare from 'react-addons-shallow-compare'; -class CustomColorPickerUI extends Component { +class CustomColorPickerUI extends PureComponent { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } - shouldComponentUpdate(nextProps, nextState) { - return shallowCompare(nextProps, nextState); - } - handleChange(data) { this.props.onChange(data); } diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.js index 6da10f6a80816..19770519ef010 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/gauge.js @@ -52,7 +52,7 @@ class GaugePanelConfigUi extends Component { this.state = { selectedTab: 'data' }; } - componentWillMount() { + UNSAFE_componentWillMount() { const { model } = this.props; const parts = {}; if ( diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/metric.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/metric.js index 705adc07e7314..526649766a008 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/metric.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/metric.js @@ -48,7 +48,7 @@ export class MetricPanelConfig extends Component { this.state = { selectedTab: 'data' }; } - componentWillMount() { + UNSAFE_componentWillMount() { const { model } = this.props; if ( !model.background_color_rules || diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/table.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/table.js index 029962f189aff..3acaa728bb50f 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/table.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/table.js @@ -51,7 +51,7 @@ export class TablePanelConfig extends Component { this.state = { selectedTab: 'data' }; } - componentWillMount() { + UNSAFE_componentWillMount() { const { model } = this.props; const parts = {}; if (!model.bar_color_rules || (model.bar_color_rules && model.bar_color_rules.length === 0)) { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/top_n.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/top_n.js index 8f5619909754e..7dbecb9e674b0 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/top_n.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/panel_config/top_n.js @@ -51,7 +51,7 @@ export class TopNPanelConfig extends Component { this.state = { selectedTab: 'data' }; } - componentWillMount() { + UNSAFE_componentWillMount() { const { model } = this.props; const parts = {}; if (!model.bar_color_rules || (model.bar_color_rules && model.bar_color_rules.length === 0)) { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/split.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/split.js index fe91cb39f4ace..d1c53899db879 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/split.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/split.js @@ -37,7 +37,7 @@ const SPLIT_MODES = { }; export class Split extends Component { - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const { model } = nextProps; if (model.split_mode === 'filters' && !model.split_filters) { this.props.onChange({ diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/config.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/config.js index d0e3acb2ef2fa..a917a93ffbf5e 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/config.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/table/config.js @@ -44,7 +44,7 @@ import { getDefaultQueryLanguage } from '../../lib/get_default_query_language'; import { QueryBarWrapper } from '../../query_bar_wrapper'; class TableSeriesConfigUI extends Component { - componentWillMount() { + UNSAFE_componentWillMount() { const { model } = this.props; if (!model.color_rules || (model.color_rules && model.color_rules.length === 0)) { this.props.onChange({ diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge.js index 68b738503c9b3..3be2e9daed58c 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge.js @@ -42,7 +42,7 @@ export class Gauge extends Component { this.handleResize = this.handleResize.bind(this); } - componentWillMount() { + UNSAFE_componentWillMount() { const check = () => { this.timeout = setTimeout(() => { const newState = calculateCoordinates(this.inner, this.resize, this.state); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge_vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge_vis.js index aa4ac99243397..d66eddae253a1 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge_vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/gauge_vis.js @@ -37,7 +37,7 @@ export class GaugeVis extends Component { this.handleResize = this.handleResize.bind(this); } - componentWillMount() { + UNSAFE_componentWillMount() { const check = () => { this.timeout = setTimeout(() => { const newState = calculateCoordinates(this.inner, this.resize, this.state); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/metric.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/metric.js index d6831769c6a6a..004d59efca333 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/metric.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/metric.js @@ -37,7 +37,7 @@ export class Metric extends Component { this.handleResize = this.handleResize.bind(this); } - componentWillMount() { + UNSAFE_componentWillMount() { const check = () => { this.timeout = setTimeout(() => { const newState = calculateCoordinates(this.inner, this.resize, this.state); diff --git a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx index 2e9a39b047e0b..db4101010f6d6 100644 --- a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx +++ b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx @@ -31,7 +31,7 @@ interface Props { } export class ExitFullScreenButton extends PureComponent { - public componentWillMount() { + public UNSAFE_componentWillMount() { chrome.setVisible(false); } diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx index ba47de73964f6..66acc9e247e63 100644 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend/vislib_vis_legend.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { act } from 'react-hooks-testing-library'; +import { act } from '@testing-library/react-hooks'; import { I18nProvider } from '@kbn/i18n/react'; import { EuiButtonGroup } from '@elastic/eui'; diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index f868e4b1f7504..f41024ed16191 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -88,7 +88,7 @@ export class IndexPatternSelect extends Component { this.fetchSelectedIndexPattern(this.props.indexPatternId); } - componentWillReceiveProps(nextProps: IndexPatternSelectProps) { + UNSAFE_componentWillReceiveProps(nextProps: IndexPatternSelectProps) { if (nextProps.indexPatternId !== this.props.indexPatternId) { this.fetchSelectedIndexPattern(nextProps.indexPatternId); } diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index cad095a6b0814..2b48bf237829c 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -102,7 +102,7 @@ export class EmbeddablePanel extends React.Component { } } - public componentWillMount() { + public UNSAFE_componentWillMount() { this.mounted = true; const { embeddable } = this.props; const { parent } = embeddable; diff --git a/src/plugins/eui_utils/public/eui_utils.test.tsx b/src/plugins/eui_utils/public/eui_utils.test.tsx index 019ca4fcbc18d..a42eba838fe23 100644 --- a/src/plugins/eui_utils/public/eui_utils.test.tsx +++ b/src/plugins/eui_utils/public/eui_utils.test.tsx @@ -18,7 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { renderHook, act } from 'react-hooks-testing-library'; +import { renderHook, act } from '@testing-library/react-hooks'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { EuiUtils } from './eui_utils'; diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx index a880d3c6cf87c..09e702c55ac78 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx +++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx @@ -35,7 +35,7 @@ class ExitFullScreenButtonUi extends PureComponent { } }; - public componentWillMount() { + public UNSAFE_componentWillMount() { document.addEventListener('keydown', this.onKeyDown, false); } diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index dde8efa7e1106..e3be0b08ab83f 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -112,7 +112,7 @@ class TableListView extends React.Component { diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 97ad71eaddd7c..51b9a5c5f1786 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -8,7 +8,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.6", - "react-dom": "^16.8.6" + "react": "^16.12.0", + "react-dom": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index ca584b4b4e771..fd0ce478eb6fb 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -8,6 +8,6 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.6" + "react": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 71545fa582c66..98df7f4b246dc 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -9,7 +9,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.6" + "react": "^16.12.0" }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index d5c97bb212ea0..32f441ba6ccda 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -9,7 +9,7 @@ "license": "Apache-2.0", "dependencies": { "@elastic/eui": "16.0.0", - "react": "^16.8.6" + "react": "^16.12.0" }, "scripts": { "kbn": "node ../../../../scripts/kbn.js", diff --git a/x-pack/dev-tools/jest/setup/setup_test.js b/x-pack/dev-tools/jest/setup/setup_test.js index 533ea58a561ac..f54be89f30955 100644 --- a/x-pack/dev-tools/jest/setup/setup_test.js +++ b/x-pack/dev-tools/jest/setup/setup_test.js @@ -10,3 +10,4 @@ */ import 'jest-styled-components'; +import '@testing-library/jest-dom/extend-expect'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index 118473a471561..9f48880090369 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -5,8 +5,7 @@ */ import React from 'react'; -import { render, wait, waitForElement } from 'react-testing-library'; -import 'react-testing-library/cleanup-after-each'; +import { render, wait, waitForElement } from '@testing-library/react'; import { ServiceOverview } from '..'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; @@ -61,16 +60,6 @@ describe('Service Overview -> View', () => { jest.resetAllMocks(); }); - // Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 - /* eslint-disable no-console */ - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - it('should render services, when list is not empty', async () => { // mock rest requests coreMock.http.get.mockResolvedValueOnce({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx index 1f3403be70aa0..a5356be72f5e4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx @@ -8,12 +8,11 @@ import React from 'react'; import { queryByLabelText, render, - queryBySelectText, getByText, getByDisplayValue, queryByDisplayValue, fireEvent -} from 'react-testing-library'; +} from '@testing-library/react'; import { omit } from 'lodash'; import { history } from '../../../../utils/history'; import { TransactionOverview } from '..'; @@ -32,16 +31,6 @@ const coreMock = ({ notifications: { toasts: { addWarning: () => {} } } } as unknown) as LegacyCoreStart; -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; -beforeAll(() => { - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; -}); - function setup({ urlParams, serviceTransactionTypes @@ -107,8 +96,8 @@ describe('TransactionOverview', () => { }); // secondType is selected in the dropdown - expect(queryBySelectText(container, 'secondType')).not.toBeNull(); - expect(queryBySelectText(container, 'firstType')).toBeNull(); + expect(queryByDisplayValue(container, 'secondType')).not.toBeNull(); + expect(queryByDisplayValue(container, 'firstType')).toBeNull(); expect(getByText(container, 'firstType')).not.toBeNull(); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx index 2ce8feb08d4ad..20125afb52f48 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { KeyValueTable } from '..'; -import { cleanup, render } from 'react-testing-library'; +import { render } from '@testing-library/react'; function getKeys(output: ReturnType) { const keys = output.getAllByTestId('dot-key'); @@ -19,8 +19,6 @@ function getValues(output: ReturnType) { } describe('KeyValueTable', () => { - afterEach(cleanup); - it('displays key and value table', () => { const data = [ { key: 'name.first', value: 'First Name' }, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js b/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js index ff8d54935e9b2..1b63274dd3cf4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { ManagedTable } from '..'; +import { UnoptimizedManagedTable } from '..'; describe('ManagedTable component', () => { let people; @@ -31,14 +31,14 @@ describe('ManagedTable component', () => { it('should render a page-full of items, with defaults', () => { expect( - shallow() + shallow() ).toMatchSnapshot(); }); it('should render when specifying initial values', () => { expect( shallow( - { - afterEach(cleanup); - it('should render a error with all sections', () => { const error = getError(); const output = render(); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 8c848722b32b2..3c851252666e0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -5,8 +5,7 @@ */ import React from 'react'; -import 'jest-dom/extend-expect'; -import { render, cleanup } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { SpanMetadata } from '..'; import { Span } from '../../../../../../typings/es_schemas/ui/Span'; import { @@ -15,7 +14,6 @@ import { } from '../../../../../utils/testHelpers'; describe('SpanMetadata', () => { - afterEach(cleanup); describe('render', () => { it('renders', () => { const span = ({ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index d503929cf04d2..1e06648f21eea 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -6,9 +6,8 @@ import React from 'react'; import { TransactionMetadata } from '..'; -import { render, cleanup } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction'; -import 'jest-dom/extend-expect'; import { expectTextsInDocument, expectTextsNotInDocument @@ -37,8 +36,6 @@ function getTransaction() { } describe('TransactionMetadata', () => { - afterEach(cleanup); - it('should render a transaction with all sections', () => { const transaction = getTransaction(); const output = render(); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx index 4398c129aa7b8..fbdd6bad3457d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx @@ -5,14 +5,12 @@ */ import React from 'react'; -import 'jest-dom/extend-expect'; -import { render, cleanup } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { MetadataTable } from '..'; import { expectTextsInDocument } from '../../../../utils/testHelpers'; import { SectionsWithRows } from '../helper'; describe('MetadataTable', () => { - afterEach(cleanup); it('shows sections', () => { const sectionsWithRows = ([ { key: 'foo', label: 'Foo', required: true }, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx index 4378c7fdeee0c..7a150f81580d8 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import 'jest-dom/extend-expect'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { Section } from '../Section'; import { expectTextsInDocument } from '../../../../utils/testHelpers'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index e9e7466cd81a8..4bb018c760f1f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -5,8 +5,7 @@ */ import React from 'react'; -import { render, fireEvent, cleanup } from 'react-testing-library'; -import 'react-testing-library/cleanup-after-each'; +import { render, fireEvent } from '@testing-library/react'; import { TransactionActionMenu } from '../TransactionActionMenu'; import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; import * as Transactions from './mockData'; @@ -38,7 +37,6 @@ describe('TransactionActionMenu component', () => { afterEach(() => { jest.clearAllMocks(); - cleanup(); }); it('should always render the discover link', async () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx index f55d8d470351c..57e634df22837 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx @@ -4,21 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cleanup, renderHook } from 'react-hooks-testing-library'; +import { renderHook } from '@testing-library/react-hooks'; import { useDelayedVisibility } from '.'; -afterEach(cleanup); - -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; -beforeAll(() => { - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; -}); - describe('useFetcher', () => { let hook; beforeEach(() => { diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts index 38f26c2ba9fbd..4763a560e0f85 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderHook } from 'react-hooks-testing-library'; +import { renderHook } from '@testing-library/react-hooks'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import * as useFetcherModule from './useFetcher'; import { useAvgDurationByBrowser } from './useAvgDurationByBrowser'; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx index 94c2ee09b5d17..36a8377c02527 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx @@ -5,22 +5,12 @@ */ import React from 'react'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { delay, tick } from '../utils/testHelpers'; import { useFetcher } from './useFetcher'; import { KibanaCoreContext } from '../../../observability/public/context/kibana_core'; import { LegacyCoreStart } from 'kibana/public'; -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; -beforeAll(() => { - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; -}); - // Wrap the hook with a provider so it can useKibanaCore const wrapper = ({ children }: { children?: React.ReactNode }) => ( { - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; -}); - // Wrap the hook with a provider so it can useKibanaCore const wrapper = ({ children }: { children?: React.ReactNode }) => ( { }; } - public async componentWillMount() { + public async UNSAFE_componentWillMount() { const tags = await this.props.libs.tags.getTagsWithIds(this.props.beat.tags); const blocksResult = await this.props.libs.configBlocks.getForTags( this.props.beat.tags, diff --git a/x-pack/legacy/plugins/beats_management/public/pages/beat/tags.tsx b/x-pack/legacy/plugins/beats_management/public/pages/beat/tags.tsx index 3cbb84dfb954b..672c0d89bb002 100644 --- a/x-pack/legacy/plugins/beats_management/public/pages/beat/tags.tsx +++ b/x-pack/legacy/plugins/beats_management/public/pages/beat/tags.tsx @@ -34,7 +34,7 @@ export class BeatTagsPage extends React.PureComponent { }; } - public async componentWillMount() { + public async UNSAFE_componentWillMount() { if (this.state.loading === true) { try { await this.props.beatsContainer.reload(); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker/time_picker.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker/time_picker.tsx index 66a7aa0639dba..fb0baa22c16f4 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker/time_picker.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/time_picker/time_picker.tsx @@ -48,7 +48,7 @@ export class TimePicker extends Component { }; // TODO: Refactor to no longer use componentWillReceiveProps since it is being deprecated - componentWillReceiveProps({ from, to }: Props) { + UNSAFE_componentWillReceiveProps({ from, to }: Props) { if (from !== this.props.from || to !== this.props.to) { this.setState({ range: { from, to }, diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx index b8779e7d44fcf..d486440c1fd7d 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { render, cleanup } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { withUnconnectedElementsLoadedTelemetry, WorkpadLoadedMetric, @@ -63,8 +63,6 @@ describe('Elements Loaded Telemetry', () => { trackMetric.mockReset(); }); - afterEach(cleanup); - it('tracks when all resolvedArgs are completed', () => { const { rerender } = render( > + } selectedOptions={[ { value: currentField.name, diff --git a/x-pack/legacy/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/legacy/plugins/graph/public/components/settings/settings.test.tsx index 43ad52abc4cc1..a615901f40e25 100644 --- a/x-pack/legacy/plugins/graph/public/components/settings/settings.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/settings/settings.test.tsx @@ -9,7 +9,7 @@ import { EuiTab, EuiListGroupItem, EuiButton, EuiAccordion, EuiFieldText } from import * as Rx from 'rxjs'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { Settings, AngularProps } from './settings'; -import { act } from 'react-testing-library'; +import { act } from '@testing-library/react'; import { ReactWrapper } from 'enzyme'; import { UrlTemplateForm } from './url_template_form'; import { diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.js b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.js index 81272c748f60a..2478dec36547c 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/edit_policy/components/node_attrs_details/node_attrs_details.js @@ -27,7 +27,7 @@ export class NodeAttrsDetails extends PureComponent { selectedNodeAttrs: PropTypes.string.isRequired, }; - componentWillMount() { + UNSAFE_componentWillMount() { this.props.fetchNodeDetails(this.props.selectedNodeAttrs); } diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts index 50dd8215102ff..4a4896347333c 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/helpers/home.helpers.ts @@ -105,7 +105,6 @@ export const setup = async (): Promise => { const { rows } = table.getMetaData('templateTable'); const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { const { href } = templateLink.props(); router.navigateTo(href!); diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts index a7c0ac4181618..9e8af02b74631 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/home.test.ts @@ -22,9 +22,7 @@ const removeWhiteSpaceOnArrayValues = (array: any[]) => jest.mock('ui/new_platform'); -// We need to skip the tests until react 16.9.0 is released -// which supports asynchronous code inside act() -describe.skip('', () => { +describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: IdxMgmtHomeTestBed; @@ -38,7 +36,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { const { component } = testBed; @@ -81,7 +78,6 @@ describe.skip('', () => { actions.selectHomeTab('templatesTab'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -101,7 +97,6 @@ describe.skip('', () => { actions.selectHomeTab('templatesTab'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -147,7 +142,6 @@ describe.skip('', () => { actions.selectHomeTab('templatesTab'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -186,7 +180,6 @@ describe.skip('', () => { expect(exists('reloadButton')).toBe(true); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickReloadButton(); await nextTick(); @@ -214,7 +207,6 @@ describe.skip('', () => { expect(exists('systemTemplatesSwitch')).toBe(true); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { form.toggleEuiSwitch('systemTemplatesSwitch'); await nextTick(); @@ -290,7 +282,6 @@ describe.skip('', () => { test('should show a warning message when attempting to delete a system template', async () => { const { component, form, actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { form.toggleEuiSwitch('systemTemplatesSwitch'); await nextTick(); @@ -328,7 +319,6 @@ describe.skip('', () => { }, }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { confirmButton!.click(); await nextTick(); @@ -384,7 +374,6 @@ describe.skip('', () => { actions.clickCloseDetailsButton(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -474,7 +463,6 @@ describe.skip('', () => { await actions.clickTemplateAt(0); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx index bd8d9b8e35675..997fe8cff2dac 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_clone.test.tsx @@ -38,9 +38,7 @@ jest.mock('@elastic/eui', () => ({ ), })); -// We need to skip the tests until react 16.9.0 is released -// which supports asynchronous code inside act() -describe.skip('', () => { +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -59,7 +57,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -77,7 +74,6 @@ describe.skip('', () => { beforeEach(async () => { const { actions, component } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 1 (logistics) // Specify index patterns, but do not change name (keep default) @@ -105,7 +101,6 @@ describe.skip('', () => { it('should send the correct payload', async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx index a391811257a9f..e678b7a7f52d6 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_create.test.tsx @@ -43,9 +43,7 @@ jest.mock('@elastic/eui', () => ({ ), })); -// We need to skip the tests until react 16.9.0 is released -// which supports asynchronous code inside act() -describe.skip('', () => { +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -71,7 +69,6 @@ describe.skip('', () => { expect(find('nextButton').props().disabled).toEqual(false); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickNextButton(); await nextTick(); @@ -90,7 +87,6 @@ describe.skip('', () => { beforeEach(async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 1 (logistics) await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); @@ -107,7 +103,6 @@ describe.skip('', () => { it('should not allow invalid json', async () => { const { form, actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.completeStepTwo('{ invalidJsonString '); }); @@ -120,7 +115,6 @@ describe.skip('', () => { beforeEach(async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 1 (logistics) await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); @@ -140,7 +134,6 @@ describe.skip('', () => { it('should not allow invalid json', async () => { const { actions, form } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 3 (mappings) with invalid json await actions.completeStepThree('{ invalidJsonString '); @@ -154,7 +147,6 @@ describe.skip('', () => { beforeEach(async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 1 (logistics) await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); @@ -177,7 +169,6 @@ describe.skip('', () => { it('should not allow invalid json', async () => { const { actions, form } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 4 (aliases) with invalid json await actions.completeStepFour('{ invalidJsonString '); @@ -194,7 +185,6 @@ describe.skip('', () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 1 (logistics) await actions.completeStepOne({ @@ -249,7 +239,6 @@ describe.skip('', () => { const { actions, exists, find } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 1 (logistics) await actions.completeStepOne({ @@ -280,7 +269,6 @@ describe.skip('', () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 1 (logistics) await actions.completeStepOne({ @@ -302,7 +290,6 @@ describe.skip('', () => { it('should send the correct payload', async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); @@ -333,7 +320,6 @@ describe.skip('', () => { httpRequestsMockHelpers.setCreateTemplateResponse(undefined, { body: error }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); diff --git a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx index 4056bd2ad63e7..975d82b936054 100644 --- a/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx +++ b/x-pack/legacy/plugins/index_management/__jest__/client_integration/template_edit.test.tsx @@ -40,9 +40,7 @@ jest.mock('@elastic/eui', () => ({ ), })); -// We need to skip the tests until react 16.9.0 is released -// which supports asynchronous code inside act() -describe.skip('', () => { +describe('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -61,7 +59,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -87,7 +84,6 @@ describe.skip('', () => { beforeEach(async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Complete step 1 (logistics) await actions.completeStepOne({ @@ -108,7 +104,6 @@ describe.skip('', () => { it('should send the correct payload with changed values', async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); diff --git a/x-pack/legacy/plugins/index_management/public/app/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js b/x-pack/legacy/plugins/index_management/public/app/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js index fe919ed7ae6b7..92b46e8e0da00 100644 --- a/x-pack/legacy/plugins/index_management/public/app/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js +++ b/x-pack/legacy/plugins/index_management/public/app/sections/home/index_list/detail_panel/edit_settings_json/edit_settings_json.js @@ -56,7 +56,7 @@ export class EditSettingsJson extends React.PureComponent { } return newSettings; } - componentWillMount() { + UNSAFE_componentWillMount() { const { indexName } = this.props; this.props.loadIndexData({ dataType: TAB_SETTINGS, indexName }); } diff --git a/x-pack/legacy/plugins/index_management/public/app/sections/home/index_list/detail_panel/show_json/show_json.js b/x-pack/legacy/plugins/index_management/public/app/sections/home/index_list/detail_panel/show_json/show_json.js index d41be90ba6ad3..854ccba2d3d19 100644 --- a/x-pack/legacy/plugins/index_management/public/app/sections/home/index_list/detail_panel/show_json/show_json.js +++ b/x-pack/legacy/plugins/index_management/public/app/sections/home/index_list/detail_panel/show_json/show_json.js @@ -10,10 +10,10 @@ import { EuiCodeEditor } from '@elastic/eui'; import 'brace/theme/textmate'; export class ShowJson extends React.PureComponent { - componentWillMount() { + UNSAFE_componentWillMount() { this.props.loadIndexData(this.props); } - componentWillUpdate(newProps) { + UNSAFE_componentWillUpdate(newProps) { const { data, loadIndexData } = newProps; if (!data) { loadIndexData(newProps); diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx index 932415bfe1afc..c43e2f5a544cf 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx @@ -8,7 +8,7 @@ import { fetch } from '../../utils/fetch'; import { useMetricsExplorerData } from './use_metrics_explorer_data'; import { MetricsExplorerAggregation } from '../../../server/routes/metrics_explorer/types'; -import { renderHook } from 'react-hooks-testing-library'; +import { renderHook } from '@testing-library/react-hooks'; import { options, diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.test.tsx b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.test.tsx index 184655398bd9c..e58184c78b4b8 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.test.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderHook, act } from 'react-hooks-testing-library'; +import { renderHook, act } from '@testing-library/react-hooks'; import { useMetricsExplorerOptions, MetricsExplorerOptionsContainer, @@ -65,7 +65,7 @@ describe('useMetricExplorerOptions', () => { }); it('should change the store when options update', () => { - const { result, waitForNextUpdate } = renderUseMetricsExplorerOptionsHook(); + const { result, rerender } = renderUseMetricsExplorerOptionsHook(); const newOptions: MetricsExplorerOptions = { ...DEFAULT_OPTIONS, metrics: [{ aggregation: MetricsExplorerAggregation.count }], @@ -73,13 +73,13 @@ describe('useMetricExplorerOptions', () => { act(() => { result.current.setOptions(newOptions); }); - waitForNextUpdate(); + rerender(); expect(result.current.options).toEqual(newOptions); expect(STORE.MetricsExplorerOptions).toEqual(JSON.stringify(newOptions)); }); it('should change the store when timerange update', () => { - const { result, waitForNextUpdate } = renderUseMetricsExplorerOptionsHook(); + const { result, rerender } = renderUseMetricsExplorerOptionsHook(); const newTimeRange: MetricsExplorerTimeOptions = { ...DEFAULT_TIMERANGE, from: 'now-15m', @@ -87,7 +87,7 @@ describe('useMetricExplorerOptions', () => { act(() => { result.current.setTimeRange(newTimeRange); }); - waitForNextUpdate(); + rerender(); expect(result.current.currentTimerange).toEqual(newTimeRange); expect(STORE.MetricsExplorerTimeRange).toEqual(JSON.stringify(newTimeRange)); }); diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx index 4c0d95c5529e8..0512fb0a46b90 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx @@ -5,7 +5,7 @@ */ import { fetch } from '../../../utils/fetch'; -import { renderHook } from 'react-hooks-testing-library'; +import { renderHook } from '@testing-library/react-hooks'; import { useMetricsExplorerState } from './use_metric_explorer_state'; import { MetricsExplorerOptionsContainer } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; import React from 'react'; @@ -172,7 +172,7 @@ describe('useMetricsExplorerState', () => { describe('handleLoadMore', () => { it('should load more based on the afterKey', async () => { - const { result, waitForNextUpdate } = renderUseMetricsExplorerStateHook(); + const { result, waitForNextUpdate, rerender } = renderUseMetricsExplorerStateHook(); expect(result.current.data).toBe(null); expect(result.current.loading).toBe(true); await waitForNextUpdate(); @@ -189,7 +189,7 @@ describe('useMetricsExplorerState', () => { } as any); const { handleLoadMore } = result.current; handleLoadMore(pageInfo.afterKey!); - await waitForNextUpdate(); + await rerender(); expect(result.current.loading).toBe(true); await waitForNextUpdate(); expect(result.current.loading).toBe(false); diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/use_host_ip_to_name.test.ts b/x-pack/legacy/plugins/infra/public/pages/link_to/use_host_ip_to_name.test.ts index 32f3864bbfe4e..3b61181dfc6e0 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/use_host_ip_to_name.test.ts +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/use_host_ip_to_name.test.ts @@ -6,7 +6,7 @@ import { useHostIpToName } from './use_host_ip_to_name'; import { fetch } from '../../utils/fetch'; -import { renderHook } from 'react-hooks-testing-library'; +import { renderHook } from '@testing-library/react-hooks'; const renderUseHostIpToNameHook = () => renderHook(props => useHostIpToName(props.ipAddress, props.indexPattern), { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index d4cf4f7ffbaa6..f615914360a35 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -9,7 +9,11 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { EuiComboBox, EuiSideNav, EuiPopover } from '@elastic/eui'; import { changeColumn } from '../state_helpers'; -import { IndexPatternDimensionPanel, IndexPatternDimensionPanelProps } from './dimension_panel'; +import { + IndexPatternDimensionPanel, + IndexPatternDimensionPanelComponent, + IndexPatternDimensionPanelProps, +} from './dimension_panel'; import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; @@ -164,7 +168,7 @@ describe('IndexPatternDimensionPanel', () => { const filterOperations = jest.fn().mockReturnValue(true); wrapper = shallow( - + ); expect(filterOperations).toBeCalled(); @@ -1076,7 +1080,7 @@ describe('IndexPatternDimensionPanel', () => { it('is not droppable if the dragged item has no field', () => { wrapper = shallow( - { it('is not droppable if field is not supported by filterOperations', () => { wrapper = shallow( - { it('is droppable if the field is supported by filterOperations', () => { wrapper = shallow( - { it('is notdroppable if the field belongs to another index pattern', () => { wrapper = shallow( - { }; const testState = dragDropState(); wrapper = shallow( - { }; const testState = dragDropState(); wrapper = shallow( - { }; const testState = dragDropState(); wrapper = shallow( - >; } -export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPanel( +export const IndexPatternDimensionPanelComponent = function IndexPatternDimensionPanel( props: IndexPatternDimensionPanelProps ) { const layerId = props.layerId; @@ -188,4 +188,6 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan ); -}); +}; + +export const IndexPatternDimensionPanel = memo(IndexPatternDimensionPanelComponent); diff --git a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.js b/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.js index 6b3bc1f106531..20b130d80a211 100644 --- a/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.js +++ b/x-pack/legacy/plugins/license_management/public/sections/license_dashboard/start_trial/start_trial.js @@ -36,7 +36,7 @@ export class StartTrial extends React.PureComponent { super(props); this.state = { showConfirmation: false }; } - componentWillMount() { + UNSAFE_componentWillMount() { this.props.loadTrialStatus(); } startLicenseTrial = () => { diff --git a/x-pack/legacy/plugins/maps/public/components/map_listing.js b/x-pack/legacy/plugins/maps/public/components/map_listing.js index 5a9cb54109363..6fb5930e81a20 100644 --- a/x-pack/legacy/plugins/maps/public/components/map_listing.js +++ b/x-pack/legacy/plugins/maps/public/components/map_listing.js @@ -44,7 +44,7 @@ export class MapListing extends React.Component { perPage: 20, }; - componentWillMount() { + UNSAFE_componentWillMount() { this._isMounted = true; } diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx index 0ac7e5eb09331..98439d76627b9 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx @@ -5,12 +5,10 @@ */ import React from 'react'; -import { cleanup, render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { SeverityCell } from './severity_cell'; describe('SeverityCell', () => { - afterEach(cleanup); - test('should render a single-bucket marker with rounded severity score', () => { const props = { score: 75.2, diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.test.js index 5f94e89ad2ba5..3f72209f22456 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/id_badges/id_badges.test.js @@ -4,37 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ - import React from 'react'; -import { cleanup, render } from 'react-testing-library'; +import { render } from '@testing-library/react'; // eslint-disable-line import/no-extraneous-dependencies import { IdBadges } from './id_badges'; - - const props = { limit: 2, maps: { groupsMap: { - 'group1': ['job1', 'job2'], - 'group2': ['job3'] + group1: ['job1', 'job2'], + group2: ['job3'], }, jobsMap: { - 'job1': ['group1'], - 'job2': ['group1'], - 'job3': ['group2'], - 'job4': [] - } + job1: ['group1'], + job2: ['group1'], + job3: ['group2'], + job4: [], + }, }, onLinkClick: jest.fn(), selectedIds: ['group1', 'job1', 'job3'], - showAllBarBadges: false + showAllBarBadges: false, }; -const overLimitProps = { ...props, selectedIds: ['group1', 'job1', 'job3', 'job4'], }; +const overLimitProps = { ...props, selectedIds: ['group1', 'job1', 'job3', 'job4'] }; describe('IdBadges', () => { - afterEach(cleanup); - test('When group selected renders groupId and not corresponding jobIds', () => { const { getByText, queryByText } = render(); // group1 badge should be present @@ -46,7 +41,6 @@ describe('IdBadges', () => { }); describe('showAllBarBadges is false', () => { - test('shows link to show more badges if selection is over limit', () => { const { getByText } = render(); const showMoreLink = getByText('And 1 more'); @@ -58,14 +52,13 @@ describe('IdBadges', () => { const showMoreLink = queryByText(/ more/); expect(showMoreLink).toBeNull(); }); - }); describe('showAllBarBadges is true', () => { const overLimitShowAllProps = { ...props, showAllBarBadges: true, - selectedIds: ['group1', 'job1', 'job3', 'job4'] + selectedIds: ['group1', 'job1', 'job3', 'job4'], }; test('shows all badges when selection is over limit', () => { @@ -86,7 +79,5 @@ describe('IdBadges', () => { const hideLink = getByText('Hide'); expect(hideLink).toBeDefined(); }); - }); - }); diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.test.js index af300e51eef99..41e510459fcea 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.test.js @@ -4,20 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ - - import React from 'react'; -import { cleanup, fireEvent, render } from 'react-testing-library'; +import { fireEvent, render } from '@testing-library/react'; // eslint-disable-line import/no-extraneous-dependencies import { JobSelectorTable } from './job_selector_table'; - jest.mock('../../../services/job_service', () => ({ mlJobService: { - getJob: jest.fn() - } + getJob: jest.fn(), + }, })); - const props = { ganttBarWidth: 299, groupsList: [ @@ -27,8 +23,8 @@ const props = { timeRange: { fromPx: 15.1, label: 'Apr 20th 2019, 20: 39 to Jun 20th 2019, 17: 45', - widthPx: 283.89 - } + widthPx: 283.89, + }, }, { id: 'ecommerce', @@ -36,8 +32,8 @@ const props = { timeRange: { fromPx: 1, label: 'Apr 17th 2019, 20:04 to May 18th 2019, 19:45', - widthPx: 144.5 - } + widthPx: 144.5, + }, }, { id: 'flights', @@ -45,9 +41,9 @@ const props = { timeRange: { fromPx: 19.6, label: 'Apr 21st 2019, 20:00 to Jun 2nd 2019, 19:50', - widthPx: 195.8 - } - } + widthPx: 195.8, + }, + }, ], jobs: [ { @@ -59,8 +55,8 @@ const props = { timeRange: { fromPx: 12.3, label: 'Apr 20th 2019, 20:39 to Jun 20th 2019, 17:45', - widthPx: 228.6 - } + widthPx: 228.6, + }, }, { groups: ['logs'], @@ -71,8 +67,8 @@ const props = { timeRange: { fromPx: 10, label: 'Apr 20th 2019, 20:39 to Jun 20th 2019, 17:45', - widthPx: 182.9 - } + widthPx: 182.9, + }, }, { groups: ['ecommerce'], @@ -83,7 +79,7 @@ const props = { timeRange: { fromPx: 1, label: 'Apr 17th 2019, 20:04 to May 18th 2019, 19:45', - widthPx: 93.1 + widthPx: 93.1, }, }, { @@ -95,19 +91,16 @@ const props = { timeRange: { fromPx: 1, label: 'Apr 17th 2019, 20:04 to May 18th 2019, 19:45', - widthPx: 93.1 + widthPx: 93.1, }, - } + }, ], onSelection: jest.fn(), selectedIds: ['price-by-day'], }; describe('JobSelectorTable', () => { - afterEach(cleanup); - describe('Single Selection', () => { - test('Does not render tabs', () => { const singleSelectionProps = { ...props, singleSelection: true }; const { queryByRole } = render(); @@ -128,28 +121,26 @@ describe('JobSelectorTable', () => { const radioButton = getByTestId('non-timeseries-job-radio-button'); expect(radioButton.firstChild.disabled).toEqual(true); }); - }); describe('Not Single Selection', () => { - test('renders tabs when not singleSelection', () => { - const { getByRole } = render(); - const tabs = getByRole('tab'); + const { getAllByRole } = render(); + const tabs = getAllByRole('tab'); expect(tabs).toBeDefined(); }); test('toggles content when tabs clicked', () => { // Default is Jobs tab so select Groups tab - const { getByText } = render(); + const { getByText, getAllByText } = render(); const groupsTab = getByText('Groups'); fireEvent.click(groupsTab); - const groupsTableHeader = getByText('jobs in group'); + const groupsTableHeader = getAllByText('jobs in group'); expect(groupsTableHeader).toBeDefined(); // switch back to Jobs tab const jobsTab = getByText('Jobs'); fireEvent.click(jobsTab); - const jobsTableHeader = getByText('job ID'); + const jobsTableHeader = getAllByText('job ID'); expect(jobsTableHeader).toBeDefined(); }); @@ -160,7 +151,10 @@ describe('JobSelectorTable', () => { }); test('incoming selectedIds are checked in the table when multiple ids', () => { - const multipleSelectedIdsProps = { ...props, selectedIds: ['price-by-day', 'bytes-by-geo-dest'] }; + const multipleSelectedIdsProps = { + ...props, + selectedIds: ['price-by-day', 'bytes-by-geo-dest'], + }; const { getByTestId } = render(); const priceByDayCheckbox = getByTestId('price-by-day-checkbox'); const bytesByGeoCheckbox = getByTestId('bytes-by-geo-dest-checkbox'); @@ -175,7 +169,5 @@ describe('JobSelectorTable', () => { const groupDropdownButton = getByText('Group'); expect(groupDropdownButton).toBeDefined(); }); - }); - }); diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js b/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js index a9e07c7e15f46..0ddeb15d5c2c7 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.js @@ -4,32 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ - import React from 'react'; -import { cleanup, render } from 'react-testing-library'; +import { render } from '@testing-library/react'; // eslint-disable-line import/no-extraneous-dependencies import { NewSelectionIdBadges } from './new_selection_id_badges'; - - const props = { limit: 2, maps: { groupsMap: { - 'group1': ['job1', 'job2'], - 'group2': ['job3'] - } + group1: ['job1', 'job2'], + group2: ['job3'], + }, }, onLinkClick: jest.fn(), onDeleteClick: jest.fn(), newSelection: ['group1', 'job1', 'job3'], - showAllBadges: false + showAllBadges: false, }; describe('NewSelectionIdBadges', () => { - afterEach(cleanup); - describe('showAllBarBadges is false', () => { - test('shows link to show more badges if selection is over limit', () => { const { getByText } = render(); const showMoreLink = getByText('And 1 more'); @@ -37,18 +31,17 @@ describe('NewSelectionIdBadges', () => { }); test('does not show link to show more badges if selection is within limit', () => { - const underLimitProps = { ...props, newSelection: ['group1', 'job1'], }; + const underLimitProps = { ...props, newSelection: ['group1', 'job1'] }; const { queryByText } = render(); const showMoreLink = queryByText(/ more/); expect(showMoreLink).toBeNull(); }); - }); describe('showAllBarBadges is true', () => { const showAllTrueProps = { ...props, - showAllBadges: true + showAllBadges: true, }; test('shows all badges when selection is over limit', () => { @@ -69,7 +62,5 @@ describe('NewSelectionIdBadges', () => { const hideLink = getByText('Hide'); expect(hideLink).toBeDefined(); }); - }); - }); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 6fd80c524f8ef..2a939d93a48b3 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -5,8 +5,7 @@ */ import React from 'react'; -import { cleanup, render } from 'react-testing-library'; -import 'jest-dom/extend-expect'; +import { render } from '@testing-library/react'; import * as CheckPrivilige from '../../../../../privilege/check_privilege'; import { queryByTestSubj } from '../../../../../util/test_utils'; @@ -22,8 +21,6 @@ jest.mock('../../../../../privilege/check_privilege', () => ({ jest.mock('ui/new_platform'); describe('DeleteAction', () => { - afterEach(cleanup); - test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => { const { container } = render(); expect(queryByTestSubj(container, 'mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled'); diff --git a/x-pack/legacy/plugins/ml/public/application/util/test_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/test_utils.ts index 00c06757beca8..5c020840182e5 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/test_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/test_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { queryHelpers, Matcher } from 'react-testing-library'; +import { queryHelpers } from '@testing-library/react'; /** * 'react-testing-library provides 'queryByTestId()' to get @@ -14,5 +14,5 @@ import { queryHelpers, Matcher } from 'react-testing-library'; * @param {Matcher} id The 'data-test-subj' id. * @returns {HTMLElement | null} */ -export const queryByTestSubj = (container: HTMLElement, id: Matcher) => - queryHelpers.queryByAttribute('data-test-subj', container, id); + +export const queryByTestSubj = queryHelpers.queryByAttribute.bind(null, 'data-test-subj'); diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js index 5443d6cbee6b5..78501aca566f6 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js @@ -47,7 +47,7 @@ export class ChartTarget extends React.Component { return (_metric) => true; } - componentWillReceiveProps(newProps) { + UNSAFE_componentWillReceiveProps(newProps) { if (this.plot && !_.isEqual(newProps, this.props)) { const { series, timeRange } = newProps; diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_visualization.js b/x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_visualization.js index 1ae997c5ebaa4..540460478cf53 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_visualization.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_visualization.js @@ -101,7 +101,7 @@ export class TimeseriesVisualization extends React.Component { } - componentWillReceiveProps(props) { + UNSAFE_componentWillReceiveProps(props) { const values = this.getLastValues(props); const currentKeys = _.keys(this.state.values); const keys = _.keys(values); diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js index 8c85c40951777..a4640fa45119b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js @@ -43,7 +43,7 @@ export class ClusterView extends React.Component { this.setState({ shardStats: stats }); }; - componentWillMount() { + UNSAFE_componentWillMount() { this.props.scope.$watch('showing', this.setShowing); this.props.scope.$watch(() => this.props.scope.pageData.shardStats, this.setShardStats); } diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js index dadb31f2cc83b..83c42c6dff37b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js @@ -35,7 +35,7 @@ export class SetupModeRenderer extends React.Component { isSettingUpNew: false, }; - componentWillMount() { + UNSAFE_componentWillMount() { const { scope, injector } = this.props; initSetupModeState(scope, injector, _oldData => { const newState = { renderState: true }; diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/index.js b/x-pack/legacy/plugins/monitoring/public/components/sparkline/index.js index 49d4dcfbf9a66..6f52a82c40820 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/sparkline/index.js +++ b/x-pack/legacy/plugins/monitoring/public/components/sparkline/index.js @@ -22,7 +22,7 @@ export class Sparkline extends React.Component { }; } - componentWillReceiveProps({ series, options }) { + UNSAFE_componentWillReceiveProps({ series, options }) { if (!isEqual(options, this.props.options)) { this.sparklineFlotChart.shutdown(); this.makeSparklineFlotChart(options); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx index b91c67b8f7d0a..c5bf910b007d0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx @@ -86,7 +86,7 @@ class EditRolePageUI extends Component { }; } - public componentWillMount() { + public UNSAFE_componentWillMount() { if (this.props.action === 'clone' && isReservedRole(this.props.role)) { this.backToRoleList(); } diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx index 910e576e6e1e7..25bd2a9d56059 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx @@ -7,7 +7,7 @@ import { ShallowWrapper, shallow } from 'enzyme'; import * as React from 'react'; -import { AreaChartBaseComponent, AreaChart } from './areachart'; +import { AreaChartBaseComponent, AreaChartComponent } from './areachart'; import { ChartSeriesData } from './common'; import { ScaleType, AreaSeries, Axis } from '@elastic/charts'; @@ -325,7 +325,7 @@ describe('AreaChart', () => { }; describe.each(chartDataSets as Array<[ChartSeriesData[]]>)('with valid data [%o]', data => { beforeAll(() => { - shallowWrapper = shallow(); + shallowWrapper = shallow(); }); it(`should render area chart`, () => { @@ -338,7 +338,7 @@ describe('AreaChart', () => { 'with invalid data [%o]', data => { beforeAll(() => { - shallowWrapper = shallow(); + shallowWrapper = shallow(); }); it(`should render a chart place holder`, () => { diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index d51f5e081468c..c644d148cc1c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -63,12 +63,15 @@ const checkIfAnyValidSeriesExist = ( Array.isArray(data) && data.some(checkIfAllTheDataInTheSeriesAreValid); // https://ela.st/multi-areaseries -export const AreaChartBaseComponent = React.memo<{ +export const AreaChartBaseComponent = ({ + data, + ...chartConfigs +}: { data: ChartSeriesData[]; width: string | null | undefined; height: string | null | undefined; configs?: ChartSeriesConfigs | undefined; -}>(({ data, ...chartConfigs }) => { +}) => { const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs); const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs); const xAxisId = getAxisId(`group-${data[0].key}-x`); @@ -113,14 +116,21 @@ export const AreaChartBaseComponent = React.memo<{

    ) : null; -}); +}; AreaChartBaseComponent.displayName = 'AreaChartBaseComponent'; -export const AreaChart = React.memo<{ +export const AreaChartBase = React.memo(AreaChartBaseComponent); + +AreaChartBase.displayName = 'AreaChartBase'; + +export const AreaChartComponent = ({ + areaChart, + configs, +}: { areaChart: ChartSeriesData[] | null | undefined; configs?: ChartSeriesConfigs | undefined; -}>(({ areaChart, configs }) => { +}) => { const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); @@ -128,7 +138,7 @@ export const AreaChart = React.memo<{ {({ measureRef, content: { height, width } }) => ( - ); -}); +}; + +AreaChartComponent.displayName = 'AreaChartComponent'; + +export const AreaChart = React.memo(AreaChartComponent); AreaChart.displayName = 'AreaChart'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx index 4b3ec577e6488..e28d330d31ba9 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx @@ -7,7 +7,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import * as React from 'react'; -import { BarChartBaseComponent, BarChart } from './barchart'; +import { BarChartBaseComponent, BarChartComponent } from './barchart'; import { ChartSeriesData } from './common'; import { BarSeries, ScaleType, Axis } from '@elastic/charts'; @@ -272,7 +272,7 @@ describe.each(chartDataSets)('BarChart with valid data [%o]', data => { let shallowWrapper: ShallowWrapper; beforeAll(() => { - shallowWrapper = shallow(); + shallowWrapper = shallow(); }); it(`should render chart`, () => { @@ -285,7 +285,7 @@ describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', data => { let shallowWrapper: ShallowWrapper; beforeAll(() => { - shallowWrapper = shallow(); + shallowWrapper = shallow(); }); it(`should render chart holder`, () => { diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index 04bedb827aa40..7218d7a497f19 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -45,12 +45,15 @@ const checkIfAnyValidSeriesExist = ( data.some(checkIfAllTheDataInTheSeriesAreValid); // Bar chart rotation: https://ela.st/chart-rotations -export const BarChartBaseComponent = React.memo<{ +export const BarChartBaseComponent = ({ + data, + ...chartConfigs +}: { data: ChartSeriesData[]; width: string | null | undefined; height: string | null | undefined; configs?: ChartSeriesConfigs | undefined; -}>(({ data, ...chartConfigs }) => { +}) => { const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs); const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs); const tickSize = getOr(0, 'configs.axis.tickSize', chartConfigs); @@ -96,14 +99,21 @@ export const BarChartBaseComponent = React.memo<{ ) : null; -}); +}; BarChartBaseComponent.displayName = 'BarChartBaseComponent'; -export const BarChart = React.memo<{ +export const BarChartBase = React.memo(BarChartBaseComponent); + +BarChartBase.displayName = 'BarChartBase'; + +export const BarChartComponent = ({ + barChart, + configs, +}: { barChart: ChartSeriesData[] | null | undefined; configs?: ChartSeriesConfigs | undefined; -}>(({ barChart, configs }) => { +}) => { const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); return checkIfAnyValidSeriesExist(barChart) ? ( @@ -126,6 +136,10 @@ export const BarChart = React.memo<{ data={barChart} /> ); -}); +}; + +BarChartComponent.displayName = 'BarChartComponent'; + +export const BarChart = React.memo(BarChartComponent); BarChart.displayName = 'BarChart'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap index bf0dfd9417875..2444fd0bc2b7d 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EmbeddedMap renders correctly against snapshot 1`] = ` +exports[`EmbeddedMapComponent renders correctly against snapshot 1`] = ` ({ timezoneProvider: () => () => 'America/New_York', })); -describe('EmbeddedMap', () => { +describe('EmbeddedMapComponent', () => { let setQuery: SetQuery; beforeEach(() => { @@ -48,7 +48,7 @@ describe('EmbeddedMap', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - ( - ({ endDate, filters, query, setQuery, startDate }) => { - const [embeddable, setEmbeddable] = React.useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(false); - const [isIndexError, setIsIndexError] = useState(false); - - const [, dispatchToaster] = useStateToaster(); - const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns(); - const [siemDefaultIndices] = useKibanaUiSetting(DEFAULT_INDEX_KEY); - - // This portalNode provided by react-reverse-portal allows us re-parent the MapToolTip within our - // own component tree instead of the embeddables (default). This is necessary to have access to - // the Redux store, theme provider, etc, which is required to register and un-register the draggable - // Search InPortal/OutPortal for implementation touch points - const portalNode = React.useMemo(() => createPortalNode(), []); - - const plugins = useKibanaPlugins(); - const core = useKibanaCore(); - - // Setup embeddables API (i.e. detach extra actions) useEffect - useEffect(() => { - try { - setupEmbeddablesAPI(plugins); - } catch (e) { - displayErrorToast(i18n.ERROR_CONFIGURING_EMBEDDABLES_API, e.message, dispatchToaster); +export const EmbeddedMapComponent = ({ + endDate, + filters, + query, + setQuery, + startDate, +}: EmbeddedMapProps) => { + const [embeddable, setEmbeddable] = React.useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const [isIndexError, setIsIndexError] = useState(false); + + const [, dispatchToaster] = useStateToaster(); + const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns(); + const [siemDefaultIndices] = useKibanaUiSetting(DEFAULT_INDEX_KEY); + + // This portalNode provided by react-reverse-portal allows us re-parent the MapToolTip within our + // own component tree instead of the embeddables (default). This is necessary to have access to + // the Redux store, theme provider, etc, which is required to register and un-register the draggable + // Search InPortal/OutPortal for implementation touch points + const portalNode = React.useMemo(() => createPortalNode(), []); + + const plugins = useKibanaPlugins(); + const core = useKibanaCore(); + + // Setup embeddables API (i.e. detach extra actions) useEffect + useEffect(() => { + try { + setupEmbeddablesAPI(plugins); + } catch (e) { + displayErrorToast(i18n.ERROR_CONFIGURING_EMBEDDABLES_API, e.message, dispatchToaster); + setIsLoading(false); + setIsError(true); + } + }, []); + + // Initial Load useEffect + useEffect(() => { + let isSubscribed = true; + async function setupEmbeddable() { + // Ensure at least one `siem:defaultIndex` index pattern exists before trying to import + const matchingIndexPatterns = kibanaIndexPatterns.filter(ip => + siemDefaultIndices.includes(ip.attributes.title) + ); + if (matchingIndexPatterns.length === 0 && isSubscribed) { setIsLoading(false); - setIsError(true); + setIsIndexError(true); + return; } - }, []); - - // Initial Load useEffect - useEffect(() => { - let isSubscribed = true; - async function setupEmbeddable() { - // Ensure at least one `siem:defaultIndex` index pattern exists before trying to import - const matchingIndexPatterns = kibanaIndexPatterns.filter(ip => - siemDefaultIndices.includes(ip.attributes.title) - ); - if (matchingIndexPatterns.length === 0 && isSubscribed) { - setIsLoading(false); - setIsIndexError(true); - return; - } - // Create & set Embeddable - try { - const embeddableObject = await createEmbeddable( - filters, - getIndexPatternTitleIdMapping(matchingIndexPatterns), - query, - startDate, - endDate, - setQuery, - portalNode, - plugins.embeddable - ); - if (isSubscribed) { - setEmbeddable(embeddableObject); - } - } catch (e) { - if (isSubscribed) { - displayErrorToast(i18n.ERROR_CREATING_EMBEDDABLE, e.message, dispatchToaster); - setIsError(true); - } + // Create & set Embeddable + try { + const embeddableObject = await createEmbeddable( + filters, + getIndexPatternTitleIdMapping(matchingIndexPatterns), + query, + startDate, + endDate, + setQuery, + portalNode, + plugins.embeddable + ); + if (isSubscribed) { + setEmbeddable(embeddableObject); } + } catch (e) { if (isSubscribed) { - setIsLoading(false); + displayErrorToast(i18n.ERROR_CREATING_EMBEDDABLE, e.message, dispatchToaster); + setIsError(true); } } - - if (!loadingKibanaIndexPatterns) { - setupEmbeddable(); + if (isSubscribed) { + setIsLoading(false); } - return () => { - isSubscribed = false; + } + + if (!loadingKibanaIndexPatterns) { + setupEmbeddable(); + } + return () => { + isSubscribed = false; + }; + }, [loadingKibanaIndexPatterns, kibanaIndexPatterns]); + + // queryExpression updated useEffect + useEffect(() => { + if (embeddable != null) { + embeddable.updateInput({ query }); + } + }, [query]); + + useEffect(() => { + if (embeddable != null) { + embeddable.updateInput({ filters }); + } + }, [filters]); + + // DateRange updated useEffect + useEffect(() => { + if (embeddable != null && startDate != null && endDate != null) { + const timeRange = { + from: new Date(startDate).toISOString(), + to: new Date(endDate).toISOString(), }; - }, [loadingKibanaIndexPatterns, kibanaIndexPatterns]); - - // queryExpression updated useEffect - useEffect(() => { - if (embeddable != null) { - embeddable.updateInput({ query }); - } - }, [query]); - - useEffect(() => { - if (embeddable != null) { - embeddable.updateInput({ filters }); - } - }, [filters]); - - // DateRange updated useEffect - useEffect(() => { - if (embeddable != null && startDate != null && endDate != null) { - const timeRange = { - from: new Date(startDate).toISOString(), - to: new Date(endDate).toISOString(), - }; - embeddable.updateInput({ timeRange }); - } - }, [startDate, endDate]); - - return isError ? null : ( - - - - - {i18n.EMBEDDABLE_HEADER_HELP} - - - - - - - - - - {embeddable != null ? ( - - ) : !isLoading && isIndexError ? ( - - ) : ( - - )} - - - ); - } -); + embeddable.updateInput({ timeRange }); + } + }, [startDate, endDate]); + + return isError ? null : ( + + + + + {i18n.EMBEDDABLE_HEADER_HELP} + + + + + + + + + + {embeddable != null ? ( + + ) : !isLoading && isIndexError ? ( + + ) : ( + + )} + + + ); +}; + +EmbeddedMapComponent.displayName = 'EmbeddedMapComponent'; + +export const EmbeddedMap = React.memo(EmbeddedMapComponent); EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx index 48a49835b284f..d32b62900431c 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; +import { IndexPatternsMissingPromptComponent } from './index_patterns_missing_prompt'; jest.mock('ui/documentation_links', () => ({ ELASTIC_WEBSITE_URL: 'https://www.elastic.co', @@ -16,7 +16,7 @@ jest.mock('ui/documentation_links', () => ({ describe('IndexPatternsMissingPrompt', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx index e71398455ee88..1e29676415d79 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx @@ -12,7 +12,7 @@ import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; import * as i18n from './translations'; -export const IndexPatternsMissingPrompt = React.memo(() => ( +export const IndexPatternsMissingPromptComponent = () => ( {i18n.ERROR_TITLE}} @@ -58,4 +58,10 @@ export const IndexPatternsMissingPrompt = React.memo(() => ( } /> -)); +); + +IndexPatternsMissingPromptComponent.displayName = 'IndexPatternsMissingPromptComponent'; + +export const IndexPatternsMissingPrompt = React.memo(IndexPatternsMissingPromptComponent); + +IndexPatternsMissingPrompt.displayName = 'IndexPatternsMissingPrompt'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap index 2a17a2aae8497..2ef4d9df89a1b 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap @@ -2,7 +2,7 @@ exports[`PointToolTipContent renders correctly against snapshot 1`] = ` - { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx index 7cdf3a545a2d6..0c416868bfb03 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx @@ -26,41 +26,46 @@ interface LineToolTipContentProps { featureProps: FeatureProperty[]; } -export const LineToolTipContent = React.memo( - ({ contextId, featureProps }) => { - const lineProps = featureProps.reduce>( - (acc, f) => ({ - ...acc, - ...{ [f._propertyKey]: Array.isArray(f._rawValue) ? f._rawValue : [f._rawValue] }, - }), - {} - ); +export const LineToolTipContentComponent = ({ + contextId, + featureProps, +}: LineToolTipContentProps) => { + const lineProps = featureProps.reduce>( + (acc, f) => ({ + ...acc, + ...{ [f._propertyKey]: Array.isArray(f._rawValue) ? f._rawValue : [f._rawValue] }, + }), + {} + ); - return ( - - - - - {i18n.SOURCE} - - - - - - - - {i18n.DESTINATION} - - - - - ); - } -); + return ( + + + + + {i18n.SOURCE} + + + + + + + + {i18n.DESTINATION} + + + + + ); +}; + +LineToolTipContentComponent.displayName = 'LineToolTipContentComponent'; + +export const LineToolTipContent = React.memo(LineToolTipContentComponent); LineToolTipContent.displayName = 'LineToolTipContent'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx index a73e6dabc68ae..13eefb252fb04 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { MapToolTip } from './map_tool_tip'; +import { MapToolTipComponent } from './map_tool_tip'; import { MapFeature } from '../types'; jest.mock('../../search_bar', () => ({ @@ -18,7 +18,7 @@ jest.mock('../../search_bar', () => ({ describe('MapToolTip', () => { test('placeholder component renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -36,7 +36,7 @@ describe('MapToolTip', () => { const loadFeatureGeometry = jest.fn(); const wrapper = shallow( - ( - ({ - addFilters, - closeTooltip, - features = [], - isLocked, - getLayerName, - loadFeatureProperties, - loadFeatureGeometry, - }) => { - const [isLoading, setIsLoading] = useState(true); - const [isLoadingNextFeature, setIsLoadingNextFeature] = useState(false); - const [isError, setIsError] = useState(false); - const [featureIndex, setFeatureIndex] = useState(0); - const [featureProps, setFeatureProps] = useState([]); - const [featureGeometry, setFeatureGeometry] = useState(null); - const [, setLayerName] = useState(''); +export const MapToolTipComponent = ({ + addFilters, + closeTooltip, + features = [], + isLocked, + getLayerName, + loadFeatureProperties, + loadFeatureGeometry, +}: MapToolTipProps) => { + const [isLoading, setIsLoading] = useState(true); + const [isLoadingNextFeature, setIsLoadingNextFeature] = useState(false); + const [isError, setIsError] = useState(false); + const [featureIndex, setFeatureIndex] = useState(0); + const [featureProps, setFeatureProps] = useState([]); + const [featureGeometry, setFeatureGeometry] = useState(null); + const [, setLayerName] = useState(''); - useEffect(() => { - // Early return if component doesn't yet have props -- result of mounting in portal before actual rendering - if ( - features.length === 0 || - getLayerName == null || - loadFeatureProperties == null || - loadFeatureGeometry == null - ) { - return; - } + useEffect(() => { + // Early return if component doesn't yet have props -- result of mounting in portal before actual rendering + if ( + features.length === 0 || + getLayerName == null || + loadFeatureProperties == null || + loadFeatureGeometry == null + ) { + return; + } - // Separate loaders for initial load vs loading next feature to keep tooltip from drastically resizing - if (!isLoadingNextFeature) { - setIsLoading(true); - } - setIsError(false); + // Separate loaders for initial load vs loading next feature to keep tooltip from drastically resizing + if (!isLoadingNextFeature) { + setIsLoading(true); + } + setIsError(false); - const fetchFeatureProps = async () => { - if (features[featureIndex] != null) { - const layerId = features[featureIndex].layerId; - const featureId = features[featureIndex].id; + const fetchFeatureProps = async () => { + if (features[featureIndex] != null) { + const layerId = features[featureIndex].layerId; + const featureId = features[featureIndex].id; - try { - const featureGeo = loadFeatureGeometry({ layerId, featureId }); - const [featureProperties, layerNameString] = await Promise.all([ - loadFeatureProperties({ layerId, featureId }), - getLayerName(layerId), - ]); + try { + const featureGeo = loadFeatureGeometry({ layerId, featureId }); + const [featureProperties, layerNameString] = await Promise.all([ + loadFeatureProperties({ layerId, featureId }), + getLayerName(layerId), + ]); - setFeatureProps(featureProperties); - setFeatureGeometry(featureGeo); - setLayerName(layerNameString); - } catch (e) { - setIsError(true); - } finally { - setIsLoading(false); - setIsLoadingNextFeature(false); - } + setFeatureProps(featureProperties); + setFeatureGeometry(featureGeo); + setLayerName(layerNameString); + } catch (e) { + setIsError(true); + } finally { + setIsLoading(false); + setIsLoadingNextFeature(false); } - }; - - fetchFeatureProps(); - }, [ - featureIndex, - features - .map(f => `${f.id}-${f.layerId}`) - .sort() - .join(), - ]); + } + }; - if (isError) { - return ( - - {i18n.MAP_TOOL_TIP_ERROR} - - ); - } + fetchFeatureProps(); + }, [ + featureIndex, + features + .map(f => `${f.id}-${f.layerId}`) + .sort() + .join(), + ]); - return isLoading && !isLoadingNextFeature ? ( + if (isError) { + return ( - - - + {i18n.MAP_TOOL_TIP_ERROR} - ) : ( - - { - if (closeTooltip != null) { - closeTooltip(); - setFeatureIndex(0); - } - }} - > -
    - {featureGeometry != null && featureGeometry.type === 'LineString' ? ( - - ) : ( - - )} - {features.length > 1 && ( - { - setFeatureIndex(featureIndex - 1); - setIsLoadingNextFeature(true); - }} - nextFeature={() => { - setFeatureIndex(featureIndex + 1); - setIsLoadingNextFeature(true); - }} - /> - )} - {isLoadingNextFeature && } -
    -
    -
    ); } -); + + return isLoading && !isLoadingNextFeature ? ( + + + + + + ) : ( + + { + if (closeTooltip != null) { + closeTooltip(); + setFeatureIndex(0); + } + }} + > +
    + {featureGeometry != null && featureGeometry.type === 'LineString' ? ( + + ) : ( + + )} + {features.length > 1 && ( + { + setFeatureIndex(featureIndex - 1); + setIsLoadingNextFeature(true); + }} + nextFeature={() => { + setFeatureIndex(featureIndex + 1); + setIsLoadingNextFeature(true); + }} + /> + )} + {isLoadingNextFeature && } +
    +
    +
    + ); +}; + +MapToolTipComponent.displayName = 'MapToolTipComponent'; + +export const MapToolTip = React.memo(MapToolTipComponent); MapToolTip.displayName = 'MapToolTip'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 567f091e78cb5..1733fb3aa7480 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { FeatureProperty } from '../types'; -import { getRenderedFieldValue, PointToolTipContent } from './point_tool_tip_content'; +import { getRenderedFieldValue, PointToolTipContentComponent } from './point_tool_tip_content'; import { TestProviders } from '../../../mock'; import { getEmptyStringTag } from '../../empty_value'; import { HostDetailsLink, IPDetailsLink } from '../../links'; @@ -39,7 +39,7 @@ describe('PointToolTipContent', () => { const wrapper = shallow( - { const wrapper = mount( - ( - ({ contextId, featureProps, closeTooltip }) => { - const featureDescriptionListItems = featureProps.map( - ({ _propertyKey: key, _rawValue: value }) => ({ - title: sourceDestinationFieldMappings[key], - description: ( - - {value != null ? ( - getRenderedFieldValue(key, item)} - /> - ) : ( - getEmptyTagValue() - )} - - ), - }) - ); +export const PointToolTipContentComponent = ({ + contextId, + featureProps, + closeTooltip, +}: PointToolTipContentProps) => { + const featureDescriptionListItems = featureProps.map( + ({ _propertyKey: key, _rawValue: value }) => ({ + title: sourceDestinationFieldMappings[key], + description: ( + + {value != null ? ( + getRenderedFieldValue(key, item)} + /> + ) : ( + getEmptyTagValue() + )} + + ), + }) + ); - return ; - } -); + return ; +}; + +PointToolTipContentComponent.displayName = 'PointToolTipContentComponent'; + +export const PointToolTipContent = React.memo(PointToolTipContentComponent); PointToolTipContent.displayName = 'PointToolTipContent'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx index f2673c17d246c..4c77570cfbc9f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { ToolTipFooter } from './tooltip_footer'; +import { ToolTipFooterComponent } from './tooltip_footer'; describe('ToolTipFilter', () => { let nextFeature = jest.fn(); @@ -20,7 +20,7 @@ describe('ToolTipFilter', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { describe('Lower bounds', () => { test('previousButton is disabled when featureIndex is 0', () => { const wrapper = mount( - { test('previousFeature is not called when featureIndex is 0', () => { const wrapper = mount( - { test('nextButton is enabled when featureIndex is < totalFeatures', () => { const wrapper = mount( - { test('nextFeature is called when featureIndex is < totalFeatures', () => { const wrapper = mount( - { describe('Upper bounds', () => { test('previousButton is enabled when featureIndex >== totalFeatures', () => { const wrapper = mount( - { test('previousFunction is called when featureIndex >== totalFeatures', () => { const wrapper = mount( - { test('nextButton is disabled when featureIndex >== totalFeatures', () => { const wrapper = mount( - { test('nextFunction is not called when featureIndex >== totalFeatures', () => { const wrapper = mount( - { describe('Within bounds, single feature', () => { test('previousButton is not enabled when only a single feature is provided', () => { const wrapper = mount( - { test('previousFunction is not called when only a single feature is provided', () => { const wrapper = mount( - { test('nextButton is not enabled when only a single feature is provided', () => { const wrapper = mount( - { test('nextFunction is not called when only a single feature is provided', () => { const wrapper = mount( - { describe('Within bounds, multiple features', () => { test('previousButton is enabled when featureIndex > 0 && featureIndex < totalFeatures', () => { const wrapper = mount( - { test('previousFunction is called when featureIndex > 0 && featureIndex < totalFeatures', () => { const wrapper = mount( - { test('nextButton is enabled when featureIndex > 0 && featureIndex < totalFeatures', () => { const wrapper = mount( - { test('nextFunction is called when featureIndex > 0 && featureIndex < totalFeatures', () => { const wrapper = mount( - void; } -export const ToolTipFooter = React.memo( - ({ featureIndex, totalFeatures, previousFeature, nextFeature }) => { - return ( - <> - - - - - {i18n.MAP_TOOL_TIP_FEATURES_FOOTER(featureIndex + 1, totalFeatures)} - - - - - - = totalFeatures - 1} - /> - - - - - ); - } -); +export const ToolTipFooterComponent = ({ + featureIndex, + totalFeatures, + previousFeature, + nextFeature, +}: MapToolTipFooterProps) => { + return ( + <> + + + + + {i18n.MAP_TOOL_TIP_FEATURES_FOOTER(featureIndex + 1, totalFeatures)} + + + + + + = totalFeatures - 1} + /> + + + + + ); +}; + +ToolTipFooterComponent.displayName = 'ToolTipFooterComponent'; + +export const ToolTipFooter = React.memo(ToolTipFooterComponent); ToolTipFooter.displayName = 'ToolTipFooter'; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx index 4e59acc4f6713..25b2427d34d6a 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx @@ -27,17 +27,8 @@ mockUseKibanaCore.mockImplementation(() => ({ const from = 1566943856794; const to = 1566857456791; -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; -describe('EventsViewer', () => { - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); +describe('EventsViewer', () => { test('it renders the "Showing..." subtitle with the expected event count', async () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx index 671711c60bd16..4d2e44f9a3d92 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx @@ -27,17 +27,7 @@ mockUseKibanaCore.mockImplementation(() => ({ const from = 1566943856794; const to = 1566857456791; -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; describe('StatefulEventsViewer', () => { - beforeAll(() => { - console.error = jest.fn(); - }); - - afterAll(() => { - console.error = originalError; - }); test('it renders the events viewer', async () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx index e943ca6f3e863..54847cda281f4 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx @@ -12,24 +12,15 @@ import { TestProviders } from '../../mock'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; -import { StatefulFieldsBrowser } from '.'; -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; -describe('StatefulFieldsBrowser', () => { - beforeAll(() => { - console.error = jest.fn(); - }); +import { StatefulFieldsBrowserComponent } from '.'; - afterAll(() => { - console.error = originalError; - }); +describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; test('it renders the Fields button, which displays the fields browser on click', () => { const wrapper = mount( - { test('it does NOT render the fields browser until the Fields button is clicked', () => { const wrapper = mount( - { test('it renders the fields browser when the Fields button is clicked', () => { const wrapper = mount( - { test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', () => { const wrapper = mount( - { test('it updates the selectedCategoryId state according to most fields returned', () => { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - ({ @@ -27,13 +27,13 @@ describe('formatted_bytes', () => { mockUseKibanaUiSetting.mockImplementation( getMockKibanaUiSetting(mockFrameworks.default_browser) ); - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); test('it renders bytes to hardcoded format when no configuration exists', () => { mockUseKibanaUiSetting.mockImplementation(() => [null]); - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.text()).toEqual('2.7MB'); }); @@ -41,7 +41,7 @@ describe('formatted_bytes', () => { mockUseKibanaUiSetting.mockImplementation( getMockKibanaUiSetting(mockFrameworks.default_browser) ); - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.text()).toEqual('2.7MB'); }); @@ -49,7 +49,7 @@ describe('formatted_bytes', () => { mockUseKibanaUiSetting.mockImplementation( getMockKibanaUiSetting(mockFrameworks.default_browser) ); - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.text()).toEqual('2.7MB'); }); @@ -57,7 +57,7 @@ describe('formatted_bytes', () => { mockUseKibanaUiSetting.mockImplementation( getMockKibanaUiSetting(mockFrameworks.bytes_short) ); - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.text()).toEqual('3MB'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx index 76d2c1ea7e3d0..408e8d7ad4d80 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx @@ -10,11 +10,15 @@ import numeral from '@elastic/numeral'; import { DEFAULT_BYTES_FORMAT } from '../../../common/constants'; import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; -export const PreferenceFormattedBytes = React.memo<{ value: string | number }>(({ value }) => { +export const PreferenceFormattedBytesComponent = ({ value }: { value: string | number }) => { const [bytesFormat] = useKibanaUiSetting(DEFAULT_BYTES_FORMAT); return ( <>{bytesFormat ? numeral(value).format(bytesFormat) : numeral(value).format('0,0.[0]b')} ); -}); +}; + +PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent'; + +export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent); PreferenceFormattedBytes.displayName = 'PreferenceFormattedBytes'; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap index 60464c46f1ac0..7f350072439c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap @@ -1,7 +1,55 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MarkdownHint rendering it renders the expected hints 1`] = ` - +exports[`MarkdownHintComponent rendering it renders the expected hints 1`] = ` + + + # heading + + + **bold** + + + _italics_ + + + \`code\` + + + [link](url) + + + * bullet + + + \`\`\`preformatted\`\`\` + + + >quote + + ~~ + + strikethrough + + ~~ + + ![image](url) + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx index 6319af3e6ffa1..c3268270919e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx @@ -8,11 +8,11 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { MarkdownHint } from './markdown_hint'; +import { MarkdownHintComponent } from './markdown_hint'; -describe('MarkdownHint', () => { +describe.skip('MarkdownHintComponent ', () => { test('it has inline visibility when show is true', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule( 'visibility', @@ -21,7 +21,7 @@ describe('MarkdownHint', () => { }); test('it has hidden visibility when show is false', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule( 'visibility', @@ -30,7 +30,7 @@ describe('MarkdownHint', () => { }); test('it renders the heading hint', () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper @@ -41,7 +41,7 @@ describe('MarkdownHint', () => { }); test('it renders the bold hint with a bold font-weight', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="bold-hint"]').first()).toHaveStyleRule( 'font-weight', @@ -50,7 +50,7 @@ describe('MarkdownHint', () => { }); test('it renders the italic hint with an italic font-style', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="italic-hint"]').first()).toHaveStyleRule( 'font-style', @@ -59,7 +59,7 @@ describe('MarkdownHint', () => { }); test('it renders the code hint with a monospace font family', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="code-hint"]').first()).toHaveStyleRule( 'font-family', @@ -68,7 +68,7 @@ describe('MarkdownHint', () => { }); test('it renders the preformatted hint with a monospace font family', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="preformatted-hint"]').first()).toHaveStyleRule( 'font-family', @@ -77,7 +77,7 @@ describe('MarkdownHint', () => { }); test('it renders the strikethrough hint with a line-through text-decoration', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="strikethrough-hint"]').first()).toHaveStyleRule( 'text-decoration', @@ -87,7 +87,7 @@ describe('MarkdownHint', () => { describe('rendering', () => { test('it renders the expected hints', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx index 18f3a35a23f7f..5ecd1d4c9d2ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx @@ -6,7 +6,6 @@ import { EuiText } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import * as i18n from './translations'; @@ -62,7 +61,7 @@ const TrailingWhitespace = styled.span` TrailingWhitespace.displayName = 'TrailingWhitespace'; -export const MarkdownHint = pure<{ show: boolean }>(({ show }) => ( +export const MarkdownHintComponent = ({ show }: { show: boolean }) => ( (({ show }) => ( {'~~'} {i18n.MARKDOWN_HINT_IMAGE_URL} -)); +); + +MarkdownHintComponent.displayName = 'MarkdownHintComponent'; + +export const MarkdownHint = React.memo(MarkdownHintComponent); MarkdownHint.displayName = 'MarkdownHint'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx index e3894ee6e7c66..c401075af42ce 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx @@ -7,13 +7,17 @@ import React from 'react'; import toJson from 'enzyme-to-json'; import { shallow, mount } from 'enzyme'; -import { EntityDraggable } from './entity_draggable'; +import { EntityDraggableComponent } from './entity_draggable'; import { TestProviders } from '../../mock/test_providers'; describe('entity_draggable', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -21,7 +25,11 @@ describe('entity_draggable', () => { test('renders with entity name with entity value as text', () => { const wrapper = mount( - + ); expect(wrapper.text()).toEqual('entity-name: "entity-value"'); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.tsx index d7f25c49fd7ca..b0636b08a5634 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.tsx @@ -16,37 +16,43 @@ interface Props { entityValue: string; } -export const EntityDraggable = React.memo( - ({ idPrefix, entityName, entityValue }): JSX.Element => { - const id = escapeDataProviderId(`entity-draggable-${idPrefix}-${entityName}-${entityValue}`); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - <>{`${entityName}: "${entityValue}"`} - ) - } - /> - ); - } -); +export const EntityDraggableComponent = ({ + idPrefix, + entityName, + entityValue, +}: Props): JSX.Element => { + const id = escapeDataProviderId(`entity-draggable-${idPrefix}-${entityName}-${entityValue}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + <>{`${entityName}: "${entityValue}"`} + ) + } + /> + ); +}; + +EntityDraggableComponent.displayName = 'EntityDraggableComponent'; + +export const EntityDraggable = React.memo(EntityDraggableComponent); EntityDraggable.displayName = 'EntityDraggable'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx index 3509d92ce7051..a28077ba63ddd 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx @@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; -import { AnomalyScore } from './anomaly_score'; +import { AnomalyScoreComponent } from './anomaly_score'; import { mockAnomalies } from '../mock'; import { TestProviders } from '../../../mock/test_providers'; import { Anomalies } from '../types'; @@ -26,7 +26,7 @@ describe('anomaly_scores', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { test('should not show a popover on initial render', () => { const wrapper = mount( - { test('show a popover on a mouse click', () => { const wrapper = mount( - ( - ({ jobKey, startDate, endDate, index = 0, score, interval, narrowDateRange }): JSX.Element => { - const [isOpen, setIsOpen] = useState(false); - return ( - <> - - { + const [isOpen, setIsOpen] = useState(false); + return ( + <> + + + + + setIsOpen(!isOpen)} + closePopover={() => setIsOpen(!isOpen)} + button={} + > + - - - setIsOpen(!isOpen)} - closePopover={() => setIsOpen(!isOpen)} - button={} - > - - - - - ); - } -); + + + + ); +}; + +AnomalyScoreComponent.displayName = 'AnomalyScoreComponent'; + +export const AnomalyScore = React.memo(AnomalyScoreComponent); AnomalyScore.displayName = 'AnomalyScore'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx index 17d36ffcc9099..5bd11169e4840 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx @@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; -import { AnomalyScores, createJobKey } from './anomaly_scores'; +import { AnomalyScoresComponent, createJobKey } from './anomaly_scores'; import { mockAnomalies } from '../mock'; import { TestProviders } from '../../../mock/test_providers'; import { getEmptyValue } from '../../empty_value'; @@ -28,7 +28,7 @@ describe('anomaly_scores', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { test('renders spinner when isLoading is true is passed', () => { const wrapper = mount( - { test('does NOT render spinner when isLoading is false is passed', () => { const wrapper = mount( - { test('renders an empty value if anomalies is null', () => { const wrapper = mount( - { anomalies.anomalies = []; const wrapper = mount( - { test('should not show a popover on initial render', () => { const wrapper = mount( - { test('showing a popover on a mouse click', () => { const wrapper = mount( - `${score.jobId}-${score.severity}-${score.entityName}-${score.entityValue}`; -export const AnomalyScores = React.memo( - ({ anomalies, startDate, endDate, isLoading, narrowDateRange, limit }): JSX.Element => { - if (isLoading) { - return ; - } else if (anomalies == null || anomalies.anomalies.length === 0) { - return getEmptyTagValue(); - } else { - return ( - <> - - {getTopSeverityJobs(anomalies.anomalies, limit).map((score, index) => { - const jobKey = createJobKey(score); - return ( - - ); - })} - - - ); - } +export const AnomalyScoresComponent = ({ + anomalies, + startDate, + endDate, + isLoading, + narrowDateRange, + limit, +}: Args): JSX.Element => { + if (isLoading) { + return ; + } else if (anomalies == null || anomalies.anomalies.length === 0) { + return getEmptyTagValue(); + } else { + return ( + <> + + {getTopSeverityJobs(anomalies.anomalies, limit).map((score, index) => { + const jobKey = createJobKey(score); + return ( + + ); + })} + + + ); } -); +}; + +AnomalyScoresComponent.displayName = 'AnomalyScoresComponent'; + +export const AnomalyScores = React.memo(AnomalyScoresComponent); AnomalyScores.displayName = 'AnomalyScores'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx index eec0c65c7679f..0d389ae14a825 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx @@ -9,7 +9,7 @@ import toJson from 'enzyme-to-json'; import { mockAnomalies } from '../mock'; import { cloneDeep } from 'lodash/fp'; import { shallow } from 'enzyme'; -import { DraggableScore } from './draggable_score'; +import { DraggableScoreComponent } from './draggable_score'; describe('draggable_score', () => { let anomalies = cloneDeep(mockAnomalies); @@ -20,13 +20,15 @@ describe('draggable_score', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); test('renders correctly against snapshot when the index is not included', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(toJson(wrapper)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.tsx index d156b5f0463f6..6ae31c0ac1fb9 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.tsx @@ -12,46 +12,52 @@ import { Provider } from '../../timeline/data_providers/provider'; import { Spacer } from '../../page'; import { getScoreString } from './score_health'; -export const DraggableScore = React.memo<{ +export const DraggableScoreComponent = ({ + id, + index = 0, + score, +}: { id: string; index?: number; score: Anomaly; -}>( - ({ id, index = 0, score }): JSX.Element => ( - - snapshot.isDragging ? ( - - - - ) : ( - <> - {index !== 0 && ( - <> - {','} - - - )} - {getScoreString(score.severity)} - - ) - } - /> - ) +}): JSX.Element => ( + + snapshot.isDragging ? ( + + + + ) : ( + <> + {index !== 0 && ( + <> + {','} + + + )} + {getScoreString(score.severity)} + + ) + } + /> ); +DraggableScoreComponent.displayName = 'DraggableScoreComponent'; + +export const DraggableScore = React.memo(DraggableScoreComponent); + DraggableScore.displayName = 'DraggableScore'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap index 2c4f750ffeac5..983eb2409bd77 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`JobsTable renders correctly against snapshot 1`] = ` +exports[`JobsTableComponent renders correctly against snapshot 1`] = ` { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -29,7 +29,7 @@ describe('GroupsFilterPopover', () => { test('when a filter is clicked, it becomes checked ', () => { const mockOnSelectedGroupsChanged = jest.fn(); const wrapper = mount( - diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx index e39046ba013c7..9f05ce8a5bfce 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx @@ -31,61 +31,66 @@ interface GroupsFilterPopoverProps { * @param siemJobs jobs to fetch groups from to display for filtering * @param onSelectedGroupsChanged change listener to be notified when group selection changes */ -export const GroupsFilterPopover = React.memo( - ({ siemJobs, onSelectedGroupsChanged }) => { - const [isGroupPopoverOpen, setIsGroupPopoverOpen] = useState(false); - const [selectedGroups, setSelectedGroups] = useState([]); +export const GroupsFilterPopoverComponent = ({ + siemJobs, + onSelectedGroupsChanged, +}: GroupsFilterPopoverProps) => { + const [isGroupPopoverOpen, setIsGroupPopoverOpen] = useState(false); + const [selectedGroups, setSelectedGroups] = useState([]); - const groups = siemJobs - .map(j => j.groups) - .flat() - .filter(g => g !== 'siem'); - const uniqueGroups = Array.from(new Set(groups)); + const groups = siemJobs + .map(j => j.groups) + .flat() + .filter(g => g !== 'siem'); + const uniqueGroups = Array.from(new Set(groups)); - useEffect(() => { - onSelectedGroupsChanged(selectedGroups); - }, [selectedGroups.sort().join()]); + useEffect(() => { + onSelectedGroupsChanged(selectedGroups); + }, [selectedGroups.sort().join()]); - return ( - setIsGroupPopoverOpen(!isGroupPopoverOpen)} - isSelected={isGroupPopoverOpen} - hasActiveFilters={selectedGroups.length > 0} - numActiveFilters={selectedGroups.length} - > - {i18n.GROUPS} - - } - isOpen={isGroupPopoverOpen} - closePopover={() => setIsGroupPopoverOpen(!isGroupPopoverOpen)} - panelPaddingSize="none" - > - {uniqueGroups.map((group, index) => ( - toggleSelectedGroup(group, selectedGroups, setSelectedGroups)} - > - {`${group} (${groups.filter(g => g === group).length})`} - - ))} - {uniqueGroups.length === 0 && ( - - - - -

    {i18n.NO_GROUPS_AVAILABLE}

    -
    -
    - )} -
    - ); - } -); + return ( + setIsGroupPopoverOpen(!isGroupPopoverOpen)} + isSelected={isGroupPopoverOpen} + hasActiveFilters={selectedGroups.length > 0} + numActiveFilters={selectedGroups.length} + > + {i18n.GROUPS} + + } + isOpen={isGroupPopoverOpen} + closePopover={() => setIsGroupPopoverOpen(!isGroupPopoverOpen)} + panelPaddingSize="none" + > + {uniqueGroups.map((group, index) => ( + toggleSelectedGroup(group, selectedGroups, setSelectedGroups)} + > + {`${group} (${groups.filter(g => g === group).length})`} + + ))} + {uniqueGroups.length === 0 && ( + + + + +

    {i18n.NO_GROUPS_AVAILABLE}

    +
    +
    + )} +
    + ); +}; + +GroupsFilterPopoverComponent.displayName = 'GroupsFilterPopoverComponent'; + +export const GroupsFilterPopover = React.memo(GroupsFilterPopoverComponent); GroupsFilterPopover.displayName = 'GroupsFilterPopover'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx index 5838c3105de6d..0711cc1c87966 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { JobsTableFilters } from './jobs_table_filters'; +import { JobsTableFiltersComponent } from './jobs_table_filters'; import { SiemJob } from '../../types'; import { cloneDeep } from 'lodash/fp'; import { mockSiemJobs } from '../../__mocks__/api'; @@ -20,14 +20,16 @@ describe('JobsTableFilters', () => { }); test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(toJson(wrapper)).toMatchSnapshot(); }); test('when you click Elastic Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper @@ -47,7 +49,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper @@ -67,7 +69,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter once, then Elastic Jobs filter, state is updated and selected changed', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper @@ -99,7 +101,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter twice, state is updated and it is revert', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx index ba080757d34a8..74e61f27fb2d1 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx @@ -31,65 +31,67 @@ interface JobsTableFiltersProps { * @param siemJobs jobs to fetch groups from to display for filtering * @param onFilterChanged change listener to be notified on filter changes */ -export const JobsTableFilters = React.memo( - ({ siemJobs, onFilterChanged }) => { - const [filterQuery, setFilterQuery] = useState(''); - const [selectedGroups, setSelectedGroups] = useState([]); - const [showCustomJobs, setShowCustomJobs] = useState(false); - const [showElasticJobs, setShowElasticJobs] = useState(false); +export const JobsTableFiltersComponent = ({ siemJobs, onFilterChanged }: JobsTableFiltersProps) => { + const [filterQuery, setFilterQuery] = useState(''); + const [selectedGroups, setSelectedGroups] = useState([]); + const [showCustomJobs, setShowCustomJobs] = useState(false); + const [showElasticJobs, setShowElasticJobs] = useState(false); - // Propagate filter changes to parent - useEffect(() => { - onFilterChanged({ filterQuery, showCustomJobs, showElasticJobs, selectedGroups }); - }, [filterQuery, selectedGroups.sort().join(), showCustomJobs, showElasticJobs]); + // Propagate filter changes to parent + useEffect(() => { + onFilterChanged({ filterQuery, showCustomJobs, showElasticJobs, selectedGroups }); + }, [filterQuery, selectedGroups.sort().join(), showCustomJobs, showElasticJobs]); - return ( - - - + + setFilterQuery(query.queryText.trim())} + /> + + + + + + + + + + + { + setShowElasticJobs(!showElasticJobs); + setShowCustomJobs(false); + }} + data-test-subj="show-elastic-jobs-filter-button" + withNext + > + {i18n.SHOW_ELASTIC_JOBS} + + { + setShowCustomJobs(!showCustomJobs); + setShowElasticJobs(false); }} - onChange={(query: EuiSearchBarQuery) => setFilterQuery(query.queryText.trim())} - /> - + data-test-subj="show-custom-jobs-filter-button" + > + {i18n.SHOW_CUSTOM_JOBS} + + + + + ); +}; - - - - - +JobsTableFiltersComponent.displayName = 'JobsTableFiltersComponent'; - - - { - setShowElasticJobs(!showElasticJobs); - setShowCustomJobs(false); - }} - data-test-subj="show-elastic-jobs-filter-button" - withNext - > - {i18n.SHOW_ELASTIC_JOBS} - - { - setShowCustomJobs(!showCustomJobs); - setShowElasticJobs(false); - }} - data-test-subj="show-custom-jobs-filter-button" - > - {i18n.SHOW_CUSTOM_JOBS} - - - -
    - ); - } -); +export const JobsTableFilters = React.memo(JobsTableFiltersComponent); JobsTableFilters.displayName = 'JobsTableFilters'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx index de703ca819388..91e5510f4938d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx @@ -8,7 +8,7 @@ import { shallow, mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { isChecked, isFailure, isJobLoading, JobSwitch } from './job_switch'; +import { isChecked, isFailure, isJobLoading, JobSwitchComponent } from './job_switch'; import { cloneDeep } from 'lodash/fp'; import { mockSiemJobs } from '../__mocks__/api'; import { SiemJob } from '../types'; @@ -23,7 +23,7 @@ describe('JobSwitch', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { test('should call onJobStateChange when the switch is clicked to be true/open', () => { const wrapper = mount( - { test('should have a switch when it is not in the loading state', () => { const wrapper = mount( - { test('should not have a switch when it is in the loading state', () => { const wrapper = mount( - { return failureStates.includes(jobState) || failureStates.includes(datafeedState); }; -export const JobSwitch = React.memo( - ({ job, isSiemJobsLoading, onJobStateChange }) => { - const [isLoading, setIsLoading] = useState(false); +export const JobSwitchComponent = ({ + job, + isSiemJobsLoading, + onJobStateChange, +}: JobSwitchProps) => { + const [isLoading, setIsLoading] = useState(false); - return ( - - - {isSiemJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedId) ? ( - - ) : ( - { - setIsLoading(true); - onJobStateChange(job, job.latestTimestampMs || 0, e.target.checked); - }} - showLabel={false} - label="" - /> - )} - - - ); - } -); + return ( + + + {isSiemJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedId) ? ( + + ) : ( + { + setIsLoading(true); + onJobStateChange(job, job.latestTimestampMs || 0, e.target.checked); + }} + showLabel={false} + label="" + /> + )} + + + ); +}; + +JobSwitchComponent.displayName = 'JobSwitchComponent'; + +export const JobSwitch = React.memo(JobSwitchComponent); JobSwitch.displayName = 'JobSwitch'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx index 10c9587ea10ad..691d43a8b18b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -7,12 +7,12 @@ import { shallow, mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { JobsTable } from './jobs_table'; +import { JobsTableComponent } from './jobs_table'; import { mockSiemJobs } from '../__mocks__/api'; import { cloneDeep } from 'lodash/fp'; import { SiemJob } from '../types'; -describe('JobsTable', () => { +describe('JobsTableComponent', () => { let siemJobs: SiemJob[]; let onJobStateChangeMock = jest.fn(); beforeEach(() => { @@ -22,14 +22,22 @@ describe('JobsTable', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); test('should render the hyperlink which points specifically to the job id', () => { const wrapper = mount( - + ); expect( wrapper @@ -44,7 +52,11 @@ describe('JobsTable', () => { test('should render the hyperlink with URI encodings which points specifically to the job id', () => { siemJobs[0].id = 'job id with spaces'; const wrapper = mount( - + ); expect( wrapper @@ -56,7 +68,11 @@ describe('JobsTable', () => { test('should call onJobStateChange when the switch is clicked to be true/open', () => { const wrapper = mount( - + ); wrapper .find('button[data-test-subj="job-switch"]') @@ -69,14 +85,22 @@ describe('JobsTable', () => { test('should have a switch when it is not in the loading state', () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="job-switch"]').exists()).toBe(true); }); test('should not have a switch when it is in the loading state', () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="job-switch"]').exists()).toBe(false); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx index b15c684b1bbbe..86f28ebda2086 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx @@ -93,7 +93,7 @@ export interface JobTableProps { onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => void; } -export const JobsTable = React.memo(({ isLoading, jobs, onJobStateChange }: JobTableProps) => { +export const JobsTableComponent = ({ isLoading, jobs, onJobStateChange }: JobTableProps) => { const [pageIndex, setPageIndex] = useState(0); const pageSize = 5; @@ -123,7 +123,11 @@ export const JobsTable = React.memo(({ isLoading, jobs, onJobStateChange }: JobT }} /> ); -}); +}; + +JobsTableComponent.displayName = 'JobsTableComponent'; + +export const JobsTable = React.memo(JobsTableComponent); JobsTable.displayName = 'JobsTable'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx index 6502dc909a775..2e2445fe933bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx @@ -7,11 +7,11 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { ShowingCount } from './showing_count'; +import { ShowingCountComponent } from './showing_count'; describe('ShowingCount', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx index ef8a4fb197f93..1f008ecf712ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx @@ -21,7 +21,7 @@ export interface ShowingCountProps { filterResultsLength: number; } -export const ShowingCount = React.memo(({ filterResultsLength }) => ( +export const ShowingCountComponent = ({ filterResultsLength }: ShowingCountProps) => ( (({ filterResultsLength /> -)); +); + +ShowingCountComponent.displayName = 'ShowingCountComponent'; + +export const ShowingCount = React.memo(ShowingCountComponent); ShowingCount.displayName = 'ShowingCount'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx index 198f99fdd84a2..4ea9e0cdafacb 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx @@ -8,10 +8,6 @@ import { mount } from 'enzyme'; import * as React from 'react'; import { MlPopover } from './ml_popover'; -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; - jest.mock('../../lib/settings/use_kibana_ui_setting'); jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ @@ -19,14 +15,6 @@ jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ })); describe('MlPopover', () => { - beforeAll(() => { - console.error = jest.fn(); - }); - - afterAll(() => { - console.error = originalError; - }); - test('shows upgrade popover on mouse click', () => { const wrapper = mount(); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx index 8f90877feb72f..d409f5de200a4 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx @@ -7,11 +7,11 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { PopoverDescription } from './popover_description'; +import { PopoverDescriptionComponent } from './popover_description'; describe('JobsTableFilters', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.tsx index 67a4654d8368a..c9cc1c5d4e539 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiLink, EuiText } from '@elastic/eui'; import chrome from 'ui/chrome'; -export const PopoverDescription = React.memo(() => ( +export const PopoverDescriptionComponent = () => ( ( }} /> -)); +); + +PopoverDescriptionComponent.displayName = 'PopoverDescriptionComponent'; + +export const PopoverDescription = React.memo(PopoverDescriptionComponent); PopoverDescription.displayName = 'PopoverDescription'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx index 13d48c0e62b6d..c522b7750c414 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx @@ -7,11 +7,11 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { UpgradeContents } from './upgrade_contents'; +import { UpgradeContentsComponent } from './upgrade_contents'; describe('JobsTableFilters', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.tsx index 45ea80d6a303e..a337e234f11d3 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.tsx @@ -26,50 +26,50 @@ const PopoverContentsDiv = styled.div` PopoverContentsDiv.displayName = 'PopoverContentsDiv'; -export const UpgradeContents = React.memo(() => { - return ( - - {i18n.UPGRADE_TITLE} - - - - - ), - }} - /> - - - - - - {i18n.UPGRADE_BUTTON} - - - - - {i18n.LICENSE_BUTTON} - - - - - ); -}); +export const UpgradeContentsComponent = () => ( + + {i18n.UPGRADE_TITLE} + + + + + ), + }} + /> + + + + + + {i18n.UPGRADE_BUTTON} + + + + + {i18n.LICENSE_BUTTON} + + + + +); + +export const UpgradeContents = React.memo(UpgradeContentsComponent); UpgradeContents.displayName = 'UpgradeContents'; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx index c2156bd6c046c..e84e3066e4f69 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx @@ -13,7 +13,7 @@ import { navTabsHostDetails } from '../../../pages/hosts/details/nav_tabs'; import { HostsTableType } from '../../../store/hosts/model'; import { RouteSpyState } from '../../../utils/route/types'; import { CONSTANTS } from '../../url_state/constants'; -import { TabNavigation } from './'; +import { TabNavigationComponent } from './'; import { TabNavigationProps } from './types'; describe('Tab Navigation', () => { @@ -60,12 +60,12 @@ describe('Tab Navigation', () => { }, }; test('it mounts with correct tab highlighted', () => { - const wrapper = shallow(); + const wrapper = shallow(); const hostsTab = wrapper.find('[data-test-subj="navigation-hosts"]'); expect(hostsTab.prop('isSelected')).toBeTruthy(); }); test('it changes active tab when nav changes by props', () => { - const wrapper = mount(); + const wrapper = mount(); const networkTab = () => wrapper.find('[data-test-subj="navigation-network"]').first(); expect(networkTab().prop('isSelected')).toBeFalsy(); wrapper.setProps({ @@ -77,7 +77,7 @@ describe('Tab Navigation', () => { expect(networkTab().prop('isSelected')).toBeTruthy(); }); test('it carries the url state in the link', () => { - const wrapper = shallow(); + const wrapper = shallow(); const firstTab = wrapper.find('[data-test-subj="navigation-network"]'); expect(firstTab.props().href).toBe( "#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" @@ -124,7 +124,7 @@ describe('Tab Navigation', () => { }, }; test('it mounts with correct tab highlighted', () => { - const wrapper = shallow(); + const wrapper = shallow(); const tableNavigationTab = wrapper.find( `[data-test-subj="navigation-${HostsTableType.authentications}"]` ); @@ -132,7 +132,7 @@ describe('Tab Navigation', () => { expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); }); test('it changes active tab when nav changes by props', () => { - const wrapper = mount(); + const wrapper = mount(); const tableNavigationTab = () => wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); @@ -145,7 +145,7 @@ describe('Tab Navigation', () => { expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); }); test('it carries the url state in the link', () => { - const wrapper = shallow(); + const wrapper = shallow(); const firstTab = wrapper.find( `[data-test-subj="navigation-${HostsTableType.authentications}"]` ); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index 27d10cb02a856..d405ec404b111 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -11,7 +11,7 @@ import { trackUiAction as track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../l import { getSearch } from '../helpers'; import { TabNavigationProps } from './types'; -export const TabNavigation = React.memo(props => { +export const TabNavigationComponent = (props: TabNavigationProps) => { const { display, navTabs, pageName, tabName } = props; const mapLocationToTab = (): string => { @@ -51,5 +51,10 @@ export const TabNavigation = React.memo(props => { )); return {renderTabs()}; -}); +}; + +TabNavigationComponent.displayName = 'TabNavigationComponent'; + +export const TabNavigation = React.memo(TabNavigationComponent); + TabNavigation.displayName = 'TabNavigation'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx index bd3f736bf2d19..234d5ac959c8c 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx @@ -7,7 +7,7 @@ import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; -import { render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { mockFirstLastSeenHostQuery } from '../../../../containers/hosts/first_last_seen/mock'; import { wait } from '../../../../lib/helpers'; @@ -19,31 +19,9 @@ import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; jest.mock('../../../../lib/settings/use_kibana_ui_setting'); describe('FirstLastSeen Component', () => { - // this is just a little hack to silence a warning that we'll get until react - // fixes this: https://github.com/facebook/react/pull/14853 - // For us that mean we need to upgrade to 16.9.0 - // and we will be able to do that when we are in master - const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/Warning.*not wrapped in act/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - afterAll(() => { - // eslint-disable-next-line no-console - console.error = originalError; - }); - test('Loading', async () => { const { container } = render( diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx index 63642119b430c..2cdac754198af 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx @@ -14,27 +14,6 @@ import { mockData } from './mock'; import { mockAnomalies } from '../../../ml/mock'; describe('Host Summary Component', () => { - // this is just a little hack to silence a warning that we'll get until react - // fixes this: https://github.com/facebook/react/pull/14853 - // For us that mean we need to upgrade to 16.9.0 - // and we will be able to do that when we are in master - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/Warning.*not wrapped in act/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - afterAll(() => { - // eslint-disable-next-line no-console - console.error = originalError; - }); - describe('rendering', () => { test('it renders the default Host Summary', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx index 135d45907b35e..577ec5ff51470 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx @@ -8,7 +8,7 @@ import { mockKpiHostsData, mockKpiHostDetailsData } from './mock'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import toJson from 'enzyme-to-json'; -import { KpiHostsComponent } from '.'; +import { KpiHostsComponentBase } from '.'; import * as statItems from '../../../stat_items'; import { kpiHostsMapping } from './kpi_hosts_mapping'; import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; @@ -21,7 +21,7 @@ describe('kpiHostsComponent', () => { describe('render', () => { test('it should render spinner if it is loading', () => { const wrapper: ShallowWrapper = shallow( - { test('it should render KpiHostsData', () => { const wrapper: ShallowWrapper = shallow( - { test('it should render KpiHostDetailsData', () => { const wrapper: ShallowWrapper = shallow( - { beforeEach(() => { shallow( - ( - ({ data, from, loading, id, to, narrowDateRange }) => { - const mappings = - (data as KpiHostsData).hosts !== undefined ? kpiHostsMapping : kpiHostDetailsMapping; - const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( - mappings, - data, - id, - from, - to, - narrowDateRange - ); - return loading ? ( - - - - - - ) : ( - - {statItemsProps.map((mappedStatItemProps, idx) => { - return ; - })} - - ); - } -); +export const KpiHostsComponentBase = ({ + data, + from, + loading, + id, + to, + narrowDateRange, +}: KpiHostsProps | KpiHostDetailsProps) => { + const mappings = + (data as KpiHostsData).hosts !== undefined ? kpiHostsMapping : kpiHostDetailsMapping; + const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( + mappings, + data, + id, + from, + to, + narrowDateRange + ); + return loading ? ( + + + + + + ) : ( + + {statItemsProps.map((mappedStatItemProps, idx) => { + return ; + })} + + ); +}; + +KpiHostsComponentBase.displayName = 'KpiHostsComponentBase'; + +export const KpiHostsComponent = React.memo(KpiHostsComponentBase); + +KpiHostsComponent.displayName = 'KpiHostsComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx index f8afd3aeb9dca..eb06fe8a01d79 100644 --- a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx @@ -8,7 +8,7 @@ import { getRowItemDraggables, getRowItemOverflow, getRowItemDraggable, - OverflowField, + OverflowFieldComponent, } from './helpers'; import * as React from 'react'; import { mount, shallow } from 'enzyme'; @@ -210,19 +210,21 @@ describe('Table Helpers', () => { describe('OverflowField', () => { test('it returns correctly against snapshot', () => { const overflowString = 'This string is exactly fifty-one chars in length!!!'; - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(toJson(wrapper)).toMatchSnapshot(); }); test('it does not truncates as per custom overflowLength value', () => { const overflowString = 'This string is short'; - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.text()).toBe('This string is short'); }); test('it truncates as per custom overflowLength value', () => { const overflowString = 'This string is exactly fifty-one chars in length!!!'; - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.text()).toBe('This string is exact'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/tables/helpers.tsx index b4ee93f9963e4..f4f7375c26d14 100644 --- a/x-pack/legacy/plugins/siem/public/components/tables/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/tables/helpers.tsx @@ -177,11 +177,15 @@ export const getRowItemOverflow = ( ); }; -export const Popover = React.memo<{ +export const PopoverComponent = ({ + children, + count, + idPrefix, +}: { children: React.ReactNode; count: number; idPrefix: string; -}>(({ children, count, idPrefix }) => { +}) => { const [isOpen, setIsOpen] = useState(false); return ( @@ -196,15 +200,23 @@ export const Popover = React.memo<{ ); -}); +}; + +PopoverComponent.displayName = 'PopoverComponent'; + +export const Popover = React.memo(PopoverComponent); Popover.displayName = 'Popover'; -export const OverflowField = React.memo<{ +export const OverflowFieldComponent = ({ + value, + showToolTip = true, + overflowLength = 50, +}: { value: string; showToolTip?: boolean; overflowLength?: number; -}>(({ value, showToolTip = true, overflowLength = 50 }) => ( +}) => ( {showToolTip ? ( @@ -219,6 +231,10 @@ export const OverflowField = React.memo<{ )} -)); +); + +OverflowFieldComponent.displayName = 'OverflowFieldComponent'; + +export const OverflowField = React.memo(OverflowFieldComponent); OverflowField.displayName = 'OverflowField'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx index ce465ac4f837e..35a4f4a74ae20 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx @@ -15,7 +15,7 @@ import { CloseButton } from '../actions'; import { ColumnHeaderType } from '../column_header'; import { defaultHeaders } from '../default_headers'; -import { Header } from '.'; +import { HeaderComponent } from '.'; import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers'; const filteredColumnHeader: ColumnHeaderType = 'text-filter'; @@ -30,7 +30,7 @@ describe('Header', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( -
    { test('it renders the header text', () => { const wrapper = mount( -
    { const headerSortable = { ...columnHeader, aggregatable: true }; const wrapper = mount( -
    { const wrapper = mount( -
    { const headerSortable = { ...columnHeader, aggregatable: true }; const wrapper = mount( -
    { const headerSortable = { ...columnHeader, aggregatable: false }; const wrapper = mount( -
    { const headerSortable = { ...columnHeader }; const wrapper = mount( -
    { const headerSortable = { ...columnHeader, aggregatable: undefined }; const wrapper = mount( -
    { test('truncates the header text with an ellipsis', () => { const wrapper = mount( -
    { test('it has a tooltip to display the properties of the field', () => { const wrapper = mount( -
    { const mockSetIsResizing = jest.fn(); mount( -
    ( - ({ - header, - onColumnRemoved, - onColumnResized, - onColumnSorted, - onFilterChange = noop, - setIsResizing, - sort, - }) => { - const onClick = () => { - onColumnSorted!({ - columnId: header.id, - sortDirection: getNewSortDirectionOnClick({ - clickedHeader: header, - currentSort: sort, - }), - }); - }; - - const onResize: OnResize = ({ delta, id }) => { - onColumnResized({ columnId: id, delta }); - }; - - const renderActions = (isResizing: boolean) => { - setIsResizing(isResizing); - return ( - <> - - - - - - - ); - }; +export const HeaderComponent = ({ + header, + onColumnRemoved, + onColumnResized, + onColumnSorted, + onFilterChange = noop, + setIsResizing, + sort, +}: Props) => { + const onClick = () => { + onColumnSorted!({ + columnId: header.id, + sortDirection: getNewSortDirectionOnClick({ + clickedHeader: header, + currentSort: sort, + }), + }); + }; + const onResize: OnResize = ({ delta, id }) => { + onColumnResized({ columnId: id, delta }); + }; + + const renderActions = (isResizing: boolean) => { + setIsResizing(isResizing); return ( - } - id={header.id} - onResize={onResize} - positionAbsolute - render={renderActions} - right="-1px" - top={0} - /> + <> + + + + + + ); - } -); + }; + + return ( + } + id={header.id} + onResize={onResize} + positionAbsolute + render={renderActions} + right="-1px" + top={0} + /> + ); +}; + +HeaderComponent.displayName = 'HeaderComponent'; + +export const Header = React.memo(HeaderComponent); Header.displayName = 'Header'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx index 851d48a19c2e4..370f864f51f3c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx @@ -15,7 +15,7 @@ import { mockBrowserFields } from '../../../../../public/containers/source/mock' import { Sort } from '../sort'; import { TestProviders } from '../../../../mock/test_providers'; -import { ColumnHeaders } from '.'; +import { ColumnHeadersComponent } from '.'; jest.mock('../../../resize_handle/is_resizing', () => ({ ...jest.requireActual('../../../resize_handle/is_resizing'), @@ -34,7 +34,7 @@ describe('ColumnHeaders', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { test('it renders the field browser', () => { const wrapper = mount( - { test('it renders every column header', () => { const wrapper = mount( - { test('it disables dragging during a column resize', () => { const wrapper = mount( - ( - ({ - actionsColumnWidth, - browserFields, - columnHeaders, - isEventViewer = false, - onColumnRemoved, - onColumnResized, - onColumnSorted, - onUpdateColumns, - onFilterChange = noop, - showEventsSelect, - sort, - timelineId, - toggleColumn, - }) => { - const { isResizing, setIsResizing } = useIsContainerResizing(); - - return ( - - - - {showEventsSelect && ( - - - - - - )} +export const ColumnHeadersComponent = ({ + actionsColumnWidth, + browserFields, + columnHeaders, + isEventViewer = false, + onColumnRemoved, + onColumnResized, + onColumnSorted, + onUpdateColumns, + onFilterChange = noop, + showEventsSelect, + sort, + timelineId, + toggleColumn, +}: Props) => { + const { isResizing, setIsResizing } = useIsContainerResizing(); + return ( + + + + {showEventsSelect && ( - + - + )} + + + + + + + + + + {dropProvided => ( + + {columnHeaders.map((header, i) => ( + + {(dragProvided, dragSnapshot) => ( + + {!dragSnapshot.isDragging ? ( + +
    + + ) : ( + + + + )} + + )} + + ))} + + )} + + + + ); +}; + +ColumnHeadersComponent.displayName = 'ColumnHeadersComponent'; + +export const ColumnHeaders = React.memo(ColumnHeadersComponent); - - {dropProvided => ( - - {columnHeaders.map((header, i) => ( - - {(dragProvided, dragSnapshot) => ( - - {!dragSnapshot.isDragging ? ( - -
    - - ) : ( - - - - )} - - )} - - ))} - - )} - - - - ); - } -); ColumnHeaders.displayName = 'ColumnHeaders'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx index 284cd0b49cb58..dbf6db6cd2bd9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx @@ -10,13 +10,13 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; -import { Args } from './args'; +import { ArgsComponent } from './args'; describe('Args', () => { describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( - { test('it returns an empty string when both args and process title are undefined', () => { const wrapper = mountWithIntl( - { test('it returns an empty string when both args and process title are null', () => { const wrapper = mountWithIntl( - + ); expect(wrapper.text()).toEqual(''); @@ -52,7 +57,7 @@ describe('Args', () => { test('it returns an empty string when args is an empty array, and title is an empty string', () => { const wrapper = mountWithIntl( - + ); expect(wrapper.text()).toEqual(''); @@ -61,7 +66,7 @@ describe('Args', () => { test('it returns args when args are provided, and process title is NOT provided', () => { const wrapper = mountWithIntl( - { test('it returns process title when process title is provided, and args is NOT provided', () => { const wrapper = mountWithIntl( - { test('it returns both args and process title, when both are provided', () => { const wrapper = mountWithIntl( - (({ args, contextId, eventId, processTitle }) => { +export const ArgsComponent = ({ args, contextId, eventId, processTitle }: Props) => { if (isNillEmptyOrNotFinite(args) && isNillEmptyOrNotFinite(processTitle)) { return null; } @@ -47,6 +47,10 @@ export const Args = React.memo(({ args, contextId, eventId, processTitle )} ); -}); +}; + +ArgsComponent.displayName = 'ArgsComponent'; + +export const Args = React.memo(ArgsComponent); Args.displayName = 'Args'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx index 6e8a0e8cfb17f..07b7741e5c152 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx @@ -11,7 +11,7 @@ import * as React from 'react'; import { TestProviders } from '../../../mock/test_providers'; -import { Footer, PagingControl } from './index'; +import { FooterComponent, PagingControlComponent } from './index'; import { mockData } from './mock'; describe('Footer Timeline Component', () => { @@ -23,7 +23,7 @@ describe('Footer Timeline Component', () => { describe('rendering', () => { test('it renders the default timeline footer', () => { const wrapper = shallow( -
    { test('it renders the loading panel at the beginning ', () => { const wrapper = mount( -
    { test('it renders the loadMore button if need to fetch more', () => { const wrapper = mount( -
    { test('it renders the Loading... in the more load button when fetching new data', () => { const wrapper = shallow( - { test('it renders the Load More in the more load button when fetching new data', () => { const wrapper = shallow( - { test('it does NOT render the loadMore button because there is nothing else to fetch', () => { const wrapper = mount( -
    { test('it render popover to select new itemsPerPage in timeline', () => { const wrapper = mount( -
    { test('should call loadmore when clicking on the button load more', () => { const wrapper = mount( -
    { test('Should call onChangeItemsPerPage when you pick a new limit', () => { const wrapper = mount( -
    { test('it does render the auto-refresh message instead of load more button when stream live is on', () => { const wrapper = mount( -
    { test('it does render the load more button when stream live is off', () => { const wrapper = mount( -
    void; isOpen: boolean; items: React.ReactElement[]; itemsCount: number; onClick: () => void; serverSideEventCount: number; -}>(({ closePopover, isOpen, items, itemsCount, onClick, serverSideEventCount }) => ( +}) => (
    -)); +); + +EventsCountComponent.displayName = 'EventsCountComponent'; + +export const EventsCount = React.memo(EventsCountComponent); EventsCount.displayName = 'EventsCount'; -export const PagingControl = React.memo<{ +export const PagingControlComponent = ({ + hasNextPage, + isLoading, + loadMore, +}: { hasNextPage: boolean; isLoading: boolean; loadMore: () => void; -}>(({ hasNextPage, isLoading, loadMore }) => ( +}) => ( <> {hasNextPage && ( )} -)); +); + +PagingControlComponent.displayName = 'PagingControlComponent'; + +export const PagingControl = React.memo(PagingControlComponent); PagingControl.displayName = 'PagingControl'; /** Renders a loading indicator and paging controls */ -export const Footer = React.memo( - ({ - compact, - getUpdatedAt, - hasNextPage, - height, - isEventViewer, - isLive, - isLoading, - itemsCount, - itemsPerPage, - itemsPerPageOptions, - nextCursor, - onChangeItemsPerPage, - onLoadMore, - serverSideEventCount, - tieBreaker, - }) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [paginationLoading, setPaginationLoading] = useState(false); - const [updatedAt, setUpdatedAt] = useState(null); - - const loadMore = useCallback(() => { - setPaginationLoading(true); - onLoadMore(nextCursor, tieBreaker); - }, [nextCursor, tieBreaker, onLoadMore]); - - const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - - const closePopover = useCallback(() => setIsPopoverOpen(false), []); - - useEffect(() => { - if (paginationLoading && !isLoading) { - setPaginationLoading(false); - setUpdatedAt(getUpdatedAt()); - } - - if (updatedAt === null || !isLoading) { - setUpdatedAt(getUpdatedAt()); - } - }, [isLoading]); - - if (isLoading && !paginationLoading) { - return ( - - - - ); +export const FooterComponent = ({ + compact, + getUpdatedAt, + hasNextPage, + height, + isEventViewer, + isLive, + isLoading, + itemsCount, + itemsPerPage, + itemsPerPageOptions, + nextCursor, + onChangeItemsPerPage, + onLoadMore, + serverSideEventCount, + tieBreaker, +}: FooterProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [paginationLoading, setPaginationLoading] = useState(false); + const [updatedAt, setUpdatedAt] = useState(null); + + const loadMore = useCallback(() => { + setPaginationLoading(true); + onLoadMore(nextCursor, tieBreaker); + }, [nextCursor, tieBreaker, onLoadMore]); + + const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + + useEffect(() => { + if (paginationLoading && !isLoading) { + setPaginationLoading(false); + setUpdatedAt(getUpdatedAt()); } - const rowItems = - itemsPerPageOptions && - itemsPerPageOptions.map(item => ( - { - closePopover(); - onChangeItemsPerPage(item); - }} - > - {`${item} ${i18n.ROWS}`} - - )); + if (updatedAt === null || !isLoading) { + setUpdatedAt(getUpdatedAt()); + } + }, [isLoading]); + if (isLoading && !paginationLoading) { return ( - <> - - - - - - - - - - {isLive ? ( - - - {i18n.AUTO_REFRESH_ACTIVE}{' '} - - } - type="iInCircle" - /> - - - ) : ( - - )} - - - - - - - - - - - ); - }, - (prevProps, nextProps) => { - return ( - prevProps.compact === nextProps.compact && - prevProps.hasNextPage === nextProps.hasNextPage && - prevProps.height === nextProps.height && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isLive === nextProps.isLive && - prevProps.isLoading === nextProps.isLoading && - prevProps.itemsCount === nextProps.itemsCount && - prevProps.itemsPerPage === nextProps.itemsPerPage && - prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && - prevProps.serverSideEventCount === nextProps.serverSideEventCount + + + ); } + + const rowItems = + itemsPerPageOptions && + itemsPerPageOptions.map(item => ( + { + closePopover(); + onChangeItemsPerPage(item); + }} + > + {`${item} ${i18n.ROWS}`} + + )); + + return ( + <> + + + + + + + + + + {isLive ? ( + + + {i18n.AUTO_REFRESH_ACTIVE}{' '} + + } + type="iInCircle" + /> + + + ) : ( + + )} + + + + + + + + + + + ); +}; + +FooterComponent.displayName = 'FooterComponent'; + +export const Footer = React.memo( + FooterComponent, + (prevProps, nextProps) => + prevProps.compact === nextProps.compact && + prevProps.hasNextPage === nextProps.hasNextPage && + prevProps.height === nextProps.height && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isLive === nextProps.isLive && + prevProps.isLoading === nextProps.isLoading && + prevProps.itemsCount === nextProps.itemsCount && + prevProps.itemsPerPage === nextProps.itemsPerPage && + prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && + prevProps.serverSideEventCount === nextProps.serverSideEventCount ); Footer.displayName = 'Footer'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 6219fb076635b..83564fbdc0988 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -15,7 +15,7 @@ import { TestProviders } from '../../../mock/test_providers'; import { mockUiSettings } from '../../../mock/ui_settings'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; -import { TimelineHeader } from '.'; +import { TimelineHeaderComponent } from '.'; const mockUseKibanaCore = useKibanaCore as jest.Mock; jest.mock('../../../lib/compose/kibana_core'); @@ -30,7 +30,7 @@ describe('Header', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { test('it renders the data providers', () => { const wrapper = mount( - { test('it renders the unauthorized call out providers', () => { const wrapper = mount( - ( - ({ - browserFields, - id, - indexPattern, - dataProviders, - onChangeDataProviderKqlQuery, - onChangeDroppableAndProvider, - onDataProviderEdited, - onDataProviderRemoved, - onToggleDataProviderEnabled, - onToggleDataProviderExcluded, - show, - showCallOutUnauthorizedMsg, - }) => ( - - {showCallOutUnauthorizedMsg && ( - - )} - ( + + {showCallOutUnauthorizedMsg && ( + - - - ) + )} + + + ); +TimelineHeaderComponent.displayName = 'TimelineHeaderComponent'; + +export const TimelineHeader = React.memo(TimelineHeaderComponent); + TimelineHeader.displayName = 'TimelineHeader'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index a52d4ce38ccb2..ebf4ceceafe34 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -23,7 +23,7 @@ import { ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME, } from './data_providers/provider_item_actions'; -import { Timeline } from './timeline'; +import { TimelineComponent } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; @@ -53,7 +53,7 @@ describe('Timeline', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - { const wrapper = mount( - ( - ({ - browserFields, - columns, +export const TimelineComponent = ({ + browserFields, + columns, + dataProviders, + end, + filters, + flyoutHeaderHeight, + flyoutHeight, + id, + indexPattern, + isLive, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + kqlQueryExpression, + onChangeDataProviderKqlQuery, + onChangeDroppableAndProvider, + onChangeItemsPerPage, + onDataProviderEdited, + onDataProviderRemoved, + onToggleDataProviderEnabled, + onToggleDataProviderExcluded, + show, + showCallOutUnauthorizedMsg, + start, + sort, + toggleColumn, +}: Props) => { + const core = useKibanaCore(); + const combinedQueries = combineQueries({ + config: esQuery.getEsQueryConfig(core.uiSettings), dataProviders, - end, - filters, - flyoutHeaderHeight, - flyoutHeight, - id, indexPattern, - isLive, - itemsPerPage, - itemsPerPageOptions, + browserFields, + filters, + kqlQuery: { query: kqlQueryExpression, language: 'kuery' }, kqlMode, - kqlQueryExpression, - onChangeDataProviderKqlQuery, - onChangeDroppableAndProvider, - onChangeItemsPerPage, - onDataProviderEdited, - onDataProviderRemoved, - onToggleDataProviderEnabled, - onToggleDataProviderExcluded, - show, - showCallOutUnauthorizedMsg, start, - sort, - toggleColumn, - }) => { - const core = useKibanaCore(); - const combinedQueries = combineQueries({ - config: esQuery.getEsQueryConfig(core.uiSettings), - dataProviders, - indexPattern, - browserFields, - filters, - kqlQuery: { query: kqlQueryExpression, language: 'kuery' }, - kqlMode, - start, - end, - }); - const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; - return ( - - {({ measureRef, content: { height: timelineHeaderHeight = 0, width = 0 } }) => ( - - - - - - {combinedQueries != null ? ( - c.id)} - sourceId="default" - limit={itemsPerPage} - filterQuery={combinedQueries.filterQuery} - sortField={{ - sortFieldId: sort.columnId, - direction: sort.sortDirection as Direction, - }} - > - {({ - events, - inspect, - loading, - totalCount, - pageInfo, - loadMore, - getUpdatedAt, - refetch, - }) => ( - - - -
    - - )} - - ) : null} - - )} - - ); - } -); + end, + }); + const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + return ( + + {({ measureRef, content: { height: timelineHeaderHeight = 0, width = 0 } }) => ( + + + + + + {combinedQueries != null ? ( + c.id)} + sourceId="default" + limit={itemsPerPage} + filterQuery={combinedQueries.filterQuery} + sortField={{ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }} + > + {({ + events, + inspect, + loading, + totalCount, + pageInfo, + loadMore, + getUpdatedAt, + refetch, + }) => ( + + + +
    + + )} + + ) : null} + + )} + + ); +}; + +TimelineComponent.displayName = 'TimelineComponent'; + +export const Timeline = React.memo(TimelineComponent); Timeline.displayName = 'Timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx index 8c5a08fdf5e21..2d9813206bb1e 100644 --- a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx @@ -10,7 +10,8 @@ import * as React from 'react'; import { TruncatableText } from '.'; -describe('TruncatableText', () => { +// No style rules found on passed Component +describe.skip('TruncatableText', () => { test('renders correctly against snapshot', () => { const wrapper = shallow({'Hiding in plain sight'}); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx index b397e50201f14..8dcce36e1a409 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { ImportRuleModal } from './index'; +import { ImportRuleModalComponent } from './index'; import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; import { getMockKibanaUiSetting, MockFrameworks } from '../../../../../mock'; import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; @@ -23,7 +23,11 @@ describe('ImportRuleModal', () => { getMockKibanaUiSetting((DEFAULT_KBN_VERSION as unknown) as MockFrameworks) ); const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index fdcf6263f414f..4c0f477ab525e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -47,114 +47,116 @@ interface ImportRuleModalProps { * @param payload JSON string to write to file * */ -export const ImportRuleModal = React.memo( - ({ showModal, closeModal, importComplete }) => { - const [selectedFiles, setSelectedFiles] = useState(null); - const [isImporting, setIsImporting] = useState(false); - const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); - const [, dispatchToaster] = useStateToaster(); - - const cleanupAndCloseModal = () => { - setIsImporting(false); - setSelectedFiles(null); - closeModal(); - }; - - const importRules = useCallback(async () => { - if (selectedFiles != null) { - setIsImporting(true); - const reader = new FileReader(); - reader.onload = async event => { - // @ts-ignore type is string, not ArrayBuffer as FileReader.readAsText is called - const importedRules = ndjsonToJSON(event?.target?.result ?? ''); - - const decodedRules = pipe( - RulesSchema.decode(importedRules), - fold(errors => { - cleanupAndCloseModal(); - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title: i18n.IMPORT_FAILED, - color: 'danger', - iconType: 'alert', - errors: failure(errors), - }, - }); - throw new Error(failure(errors).join('\n')); - }, identity) - ); - - const duplicatedRules = await duplicateRules({ rules: decodedRules, kbnVersion }); - importComplete(); - cleanupAndCloseModal(); - - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title: i18n.SUCCESSFULLY_IMPORTED_RULES(duplicatedRules.length), - color: 'success', - iconType: 'check', - }, - }); - }; - Object.values(selectedFiles).map(f => reader.readAsText(f)); - } - }, [selectedFiles]); - - return ( - <> - {showModal && ( - - - - {i18n.IMPORT_RULE} - - - - -

    {i18n.SELECT_RULE}

    -
    - - - { - setSelectedFiles(Object.keys(files).length > 0 ? files : null); - }} - display={'large'} - fullWidth={true} - isLoading={isImporting} - /> - - {}} - /> -
    - - - {i18n.CANCEL_BUTTON} - - {i18n.IMPORT_RULE} - - -
    -
    - )} - - ); - } -); +export const ImportRuleModalComponent = ({ + showModal, + closeModal, + importComplete, +}: ImportRuleModalProps) => { + const [selectedFiles, setSelectedFiles] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + const cleanupAndCloseModal = () => { + setIsImporting(false); + setSelectedFiles(null); + closeModal(); + }; + + const importRules = useCallback(async () => { + if (selectedFiles != null) { + setIsImporting(true); + const reader = new FileReader(); + reader.onload = async event => { + // @ts-ignore type is string, not ArrayBuffer as FileReader.readAsText is called + const importedRules = ndjsonToJSON(event?.target?.result ?? ''); + + const decodedRules = pipe( + RulesSchema.decode(importedRules), + fold(errors => { + cleanupAndCloseModal(); + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.IMPORT_FAILED, + color: 'danger', + iconType: 'alert', + errors: failure(errors), + }, + }); + throw new Error(failure(errors).join('\n')); + }, identity) + ); + + const duplicatedRules = await duplicateRules({ rules: decodedRules, kbnVersion }); + importComplete(); + cleanupAndCloseModal(); + + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_IMPORTED_RULES(duplicatedRules.length), + color: 'success', + iconType: 'check', + }, + }); + }; + Object.values(selectedFiles).map(f => reader.readAsText(f)); + } + }, [selectedFiles]); + + return ( + <> + {showModal && ( + + + + {i18n.IMPORT_RULE} + + + + +

    {i18n.SELECT_RULE}

    +
    + + + { + setSelectedFiles(Object.keys(files).length > 0 ? files : null); + }} + display={'large'} + fullWidth={true} + isLoading={isImporting} + /> + + {}} + /> +
    + + + {i18n.CANCEL_BUTTON} + + {i18n.IMPORT_RULE} + + +
    +
    + )} + + ); +}; + +ImportRuleModalComponent.displayName = 'ImportRuleModalComponent'; + +export const ImportRuleModal = React.memo(ImportRuleModalComponent); ImportRuleModal.displayName = 'ImportRuleModal'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx index ef6493f89f383..d7a508e2c53e3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { JSONDownloader, jsonToNDJSON, ndjsonToJSON } from './index'; +import { JSONDownloaderComponent, jsonToNDJSON, ndjsonToJSON } from './index'; const jsonArray = [ { @@ -35,7 +35,7 @@ const ndjsonSorted = `{"created_by":"elastic","description":"Detecting root and describe('JSONDownloader', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx index e9c2c69f067cc..2810e0b5e1680 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/json_downloader/index.tsx @@ -24,30 +24,36 @@ export interface JSONDownloaderProps { * @param payload JSON string to write to file * */ -export const JSONDownloader = React.memo( - ({ filename, payload, onExportComplete }) => { - const anchorRef = useRef(null); +export const JSONDownloaderComponent = ({ + filename, + payload, + onExportComplete, +}: JSONDownloaderProps) => { + const anchorRef = useRef(null); - useEffect(() => { - if (anchorRef && anchorRef.current && payload != null) { - const blob = new Blob([jsonToNDJSON(payload)], { type: 'application/json' }); - // @ts-ignore function is not always defined -- this is for supporting IE - if (window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveBlob(blob); - } else { - const objectURL = window.URL.createObjectURL(blob); - anchorRef.current.href = objectURL; - anchorRef.current.download = filename; - anchorRef.current.click(); - window.URL.revokeObjectURL(objectURL); - } - onExportComplete(payload.length); + useEffect(() => { + if (anchorRef && anchorRef.current && payload != null) { + const blob = new Blob([jsonToNDJSON(payload)], { type: 'application/json' }); + // @ts-ignore function is not always defined -- this is for supporting IE + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob); + } else { + const objectURL = window.URL.createObjectURL(blob); + anchorRef.current.href = objectURL; + anchorRef.current.download = filename; + anchorRef.current.click(); + window.URL.revokeObjectURL(objectURL); } - }, [payload]); + onExportComplete(payload.length); + } + }, [payload]); + + return ; +}; + +JSONDownloaderComponent.displayName = 'JSONDownloaderComponent'; - return ; - } -); +export const JSONDownloader = React.memo(JSONDownloaderComponent); JSONDownloader.displayName = 'JSONDownloader'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx index 9e5f4317678e8..fcea7101ba54b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx @@ -7,12 +7,17 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { RuleSwitch } from './index'; +import { RuleSwitchComponent } from './index'; describe('RuleSwitch', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx index da58b2e076e0d..19523752f4f4a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -27,29 +27,32 @@ export interface RuleSwitchProps { /** * Basic switch component for displaying loader when enabled/disabled */ -export const RuleSwitch = React.memo( - ({ id, enabled, isLoading, onRuleStateChange }) => { - return ( - - - {isLoading ? ( - - ) : ( - { - onRuleStateChange(e.target.checked!, id); - }} - /> - )} - - - ); - } +export const RuleSwitchComponent = ({ + id, + enabled, + isLoading, + onRuleStateChange, +}: RuleSwitchProps) => ( + + + {isLoading ? ( + + ) : ( + { + onRuleStateChange(e.target.checked!, id); + }} + /> + )} + + ); +export const RuleSwitch = React.memo(RuleSwitchComponent); + RuleSwitch.displayName = 'RuleSwitch'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx index 2d0df0b6e0033..f08cee824afa7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -81,10 +81,6 @@ const mockHistory = { listen: jest.fn(), }; -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; - const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); @@ -104,13 +100,6 @@ describe('Hosts - rendering', () => { hostsPagePath: '', }; - beforeAll(() => { - console.error = jest.fn(); - }); - - afterAll(() => { - console.error = originalError; - }); beforeEach(() => { localSource = cloneDeep(mocksSource); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx index 9e599bcfedff6..ba3e8a2f37584 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx @@ -111,13 +111,8 @@ jest.mock('ui/documentation_links', () => ({ }, })); -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; - describe('Ip Details', () => { beforeAll(() => { - console.error = jest.fn(); (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => Promise.resolve({ ok: true, @@ -129,7 +124,6 @@ describe('Ip Details', () => { }); afterAll(() => { - console.error = originalError; delete (global as GlobalWithFetch).fetch; }); const state: State = mockGlobalState; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx index 477f435b84b20..75ca5a5dfe1a6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx @@ -46,243 +46,241 @@ export { getBreadcrumbs } from './utils'; const IpOverviewManage = manageQuery(IpOverview); -export const IPDetailsComponent = React.memo( - ({ - detailName, - filters, - flowTarget, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero, - setQuery, - to, - }) => { - const type = networkModel.NetworkType.details; - const narrowDateRange = useCallback( - (score, interval) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - [scoreIntervalToDateTime, setAbsoluteRangeDatePicker] - ); - const core = useKibanaCore(); +export const IPDetailsComponent = ({ + detailName, + filters, + flowTarget, + from, + isInitializing, + query, + setAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero, + setQuery, + to, +}: IPDetailsComponentProps) => { + const type = networkModel.NetworkType.details; + const narrowDateRange = useCallback( + (score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [scoreIntervalToDateTime, setAbsoluteRangeDatePicker] + ); + const core = useKibanaCore(); - useEffect(() => { - setIpDetailsTablesActivePageToZero(null); - }, [detailName]); + useEffect(() => { + setIpDetailsTablesActivePageToZero(null); + }, [detailName]); - return ( - <> - - {({ indicesExist, indexPattern }) => { - const ip = decodeIpv6(detailName); - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(core.uiSettings), - indexPattern, - queries: [query], - filters, - }); + return ( + <> + + {({ indicesExist, indexPattern }) => { + const ip = decodeIpv6(detailName); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(core.uiSettings), + indexPattern, + queries: [query], + filters, + }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + - - } - title={ip} - > - - - - - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - + + } + title={ip} + > + + - + + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + - - - - + - - - - + + + + - + + + + - - - - + - - - - + + + + - + + + + - + - + - + - + - + - + - - - - ) : ( - - + - + - ); - }} - + + ) : ( + + - - - ); - } -); + + + ); + }} + + + + + ); +}; IPDetailsComponent.displayName = 'IPDetailsComponent'; const makeMapStateToProps = () => { @@ -299,4 +297,4 @@ const makeMapStateToProps = () => { export const IPDetails = connect(makeMapStateToProps, { setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, -})(IPDetailsComponent); +})(React.memo(IPDetailsComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx index a374b0082f281..327b0fb4c1e5b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx @@ -89,17 +89,7 @@ const getMockProps = () => ({ hasMlUserPermissions: true, }); -// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 -/* eslint-disable no-console */ -const originalError = console.error; - describe('rendering - rendering', () => { - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); beforeEach(() => { localSource = cloneDeep(mocksSource); }); diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts index 3290e1341057e..79a4eeb6dc48b 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/home.helpers.ts @@ -63,7 +63,6 @@ export const setup = async (): Promise => { const { rows } = table.getMetaData(REPOSITORY_TABLE); const repositoryLink = findTestSubject(rows[index].reactWrapper, 'repositoryLink'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { const { href } = repositoryLink.props(); router.navigateTo(href!); @@ -78,7 +77,6 @@ export const setup = async (): Promise => { const lastColumn = currentRow.columns[currentRow.columns.length - 1].reactWrapper; const button = findTestSubject(lastColumn, `${action}RepositoryButton`); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { button.simulate('click'); component.update(); @@ -89,7 +87,6 @@ export const setup = async (): Promise => { const { rows } = table.getMetaData(SNAPSHOT_TABLE); const snapshotLink = findTestSubject(rows[index].reactWrapper, 'snapshotLink'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { const { href } = snapshotLink.props(); router.navigateTo(href!); diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index e2782a1358251..effab0fcadf4d 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -71,7 +71,6 @@ describe.skip('', () => { beforeEach(async () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -98,7 +97,6 @@ describe.skip('', () => { actions.selectTab('snapshots'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -119,7 +117,6 @@ describe.skip('', () => { test('should display an empty prompt', async () => { const { component, exists } = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -152,7 +149,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -198,7 +194,6 @@ describe.skip('', () => { const totalRequests = server.requests.length; expect(exists('reloadButton')).toBe(true); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickReloadButton(); await nextTick(); @@ -255,7 +250,6 @@ describe.skip('', () => { '[data-test-subj="confirmModalConfirmButton"]' ); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { confirmButton!.click(); await nextTick(); @@ -276,7 +270,6 @@ describe.skip('', () => { expect(exists('repositoryDetail')).toBe(false); await actions.clickRepositoryAt(0); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -338,7 +331,6 @@ describe.skip('', () => { const { exists, find, component } = testBed; expect(exists('repositoryDetail.verifyRepositoryButton')).toBe(true); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { find('repositoryDetail.verifyRepositoryButton').simulate('click'); await nextTick(); @@ -384,7 +376,6 @@ describe.skip('', () => { beforeEach(async () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { testBed.actions.selectTab('snapshots'); await nextTick(100); @@ -414,7 +405,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { testBed.actions.selectTab('snapshots'); await nextTick(2000); @@ -455,7 +445,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { testBed.actions.selectTab('snapshots'); await nextTick(2000); @@ -493,7 +482,6 @@ describe.skip('', () => { const repositoryLink = findTestSubject(rows[0].reactWrapper, 'repositoryLink'); const { href } = repositoryLink.props(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { router.navigateTo(href!); await nextTick(); @@ -513,7 +501,6 @@ describe.skip('', () => { const totalRequests = server.requests.length; expect(exists('reloadButton')).toBe(true); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickReloadButton(); await nextTick(); @@ -566,7 +553,6 @@ describe.skip('', () => { const { href } = find('snapshotDetail.repositoryLink').props(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { router.navigateTo(href); await nextTick(); diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index bc48d6d6312fb..3dc7d94ce67d5 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -103,7 +103,6 @@ describe.skip('', () => { test('should require at least one index', async () => { const { find, form, component } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { // Toggle "All indices" switch form.toggleEuiSwitch('allIndicesToggle', false); @@ -187,7 +186,6 @@ describe.skip('', () => { it('should send the correct payload', async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); @@ -223,7 +221,6 @@ describe.skip('', () => { httpRequestsMockHelpers.setAddPolicyResponse(undefined, { body: error }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index efcb338e6d268..e14a080137c60 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -43,7 +43,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -64,7 +63,6 @@ describe.skip('', () => { test('should use the same Form component as the "" section', async () => { testBedPolicyAdd = await setupPolicyAdd(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBedPolicyAdd.component.update(); @@ -108,7 +106,6 @@ describe.skip('', () => { it('should send the correct payload with changed values', async () => { const { actions } = testBed; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts index 35eb3e504904f..df250159c7f69 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts @@ -156,7 +156,6 @@ describe.skip('', () => { actions.selectRepositoryType(type); actions.clickNextButton(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); @@ -176,7 +175,6 @@ describe.skip('', () => { } }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickBackButton(); await nextTick(100); @@ -217,7 +215,6 @@ describe.skip('', () => { form.setInputValue('locationInput', repository.settings.location); form.selectCheckBox('compressToggle'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); @@ -251,7 +248,6 @@ describe.skip('', () => { httpRequestsMockHelpers.setSaveRepositoryResponse(undefined, { body: error }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); @@ -278,7 +274,6 @@ describe.skip('', () => { // Fill step 2 form.setInputValue('locationInput', repository.settings.location); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts index ecc39f963ca59..ef74482fc9de7 100644 --- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts @@ -39,7 +39,6 @@ describe.skip('', () => { }); testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -79,7 +78,6 @@ describe.skip('', () => { }); testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx index 9e8f1e7c1a6f4..f461100db01db 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/components/copy_saved_objects_to_space/copy_to_space_flyout.test.tsx @@ -12,7 +12,7 @@ import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; import { Space } from '../../../../../common/model/space'; import { findTestSubject } from 'test_utils/find_test_subject'; import { SelectableSpacesControl } from './selectable_spaces_control'; -import { act } from 'react-testing-library'; +import { act } from '@testing-library/react'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; import { spacesManagerMock } from '../../../../lib/mocks'; import { SpacesManager } from '../../../../lib'; diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx index 2a0ce8ca08bec..904d788b04e2c 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx @@ -5,12 +5,10 @@ */ import React from 'react'; -import { cleanup, render } from 'react-testing-library'; +import { render } from '@testing-library/react'; import { ToastNotificationText } from './toast_notification_text'; describe('ToastNotificationText', () => { - afterEach(cleanup); - test('should render the text as plain text', () => { const props = { text: 'a short text message', diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx index 97eb284c7b771..db37bc58904ec 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/overview/deprecation_logging_toggle.tsx @@ -36,7 +36,7 @@ export class DeprecationLoggingToggleUI extends React.Component< }; } - public componentWillMount() { + public UNSAFE_componentWillMount() { this.loadData(); } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_popover.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_popover.test.tsx index 8fd826cdd7f19..77d47eff1b526 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_popover.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/filter_group/__tests__/filter_popover.test.tsx @@ -13,7 +13,7 @@ import { EuiFilterSelectItem } from '@elastic/eui'; describe('FilterPopover component', () => { let props: FilterPopoverProps; let setState: jest.Mock; - let useStateSpy: jest.SpyInstance<[unknown, React.Dispatch], [unknown]>; + let useStateSpy: jest.SpyInstance<[unknown, React.Dispatch], unknown[]>; beforeEach(() => { props = { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts index 7319806737a6f..0d3ecaa7a2b9a 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts @@ -51,7 +51,6 @@ export const setup = async (): Promise => { const { rows } = testBed.table.getMetaData('watchesTable'); const watchesLink = findTestSubject(rows[index].reactWrapper, 'watchesLink'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { const { href } = watchesLink.props(); testBed.router.navigateTo(href!); @@ -67,7 +66,6 @@ export const setup = async (): Promise => { const lastColumn = currentRow.columns[currentRow.columns.length - 1].reactWrapper; const button = findTestSubject(lastColumn, `${action}WatchButton`); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { button.simulate('click'); component.update(); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts index ac4e4eab1dcc8..22d57f255ebe6 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts @@ -57,7 +57,6 @@ export const setup = async (): Promise => { const { component } = testBed; const button = testBed.find('toggleWatchActivationButton'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { button.simulate('click'); component.update(); @@ -71,7 +70,6 @@ export const setup = async (): Promise => { const lastColumn = currentRow.columns[currentRow.columns.length - 1].reactWrapper; const button = findTestSubject(lastColumn, 'acknowledgeWatchButton'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { button.simulate('click'); component.update(); @@ -82,7 +80,6 @@ export const setup = async (): Promise => { const { component } = testBed; const button = testBed.find('deleteWatchButton'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { button.simulate('click'); component.update(); @@ -97,7 +94,6 @@ export const setup = async (): Promise => { const button = findTestSubject(firstColumn, `watchStartTimeColumn-${tableCellText}`); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { button.simulate('click'); await nextTick(100); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts index fbcd940ed65bb..f45dbe156723b 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts @@ -31,7 +31,6 @@ describe.skip(' create route', () => { beforeEach(async () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { const { component } = testBed; await nextTick(); @@ -95,7 +94,6 @@ describe.skip(' create route', () => { form.setInputValue('nameInput', watch.name); form.setInputValue('idInput', watch.id); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); @@ -144,7 +142,6 @@ describe.skip(' create route', () => { httpRequestsMockHelpers.setSaveWatchResponse(watch.id, undefined, { body: error }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); @@ -173,7 +170,6 @@ describe.skip(' create route', () => { watch: { id, type }, } = WATCH; - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); @@ -234,7 +230,6 @@ describe.skip(' create route', () => { }, }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index aa7cca6774548..62cfd92182091 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -106,7 +106,6 @@ describe.skip(' create route', () => { beforeEach(async () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { const { component } = testBed; await nextTick(); @@ -129,7 +128,6 @@ describe.skip(' create route', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -182,7 +180,6 @@ describe.skip(' create route', () => { find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox form.setInputValue('watchTimeFieldSelect', '@timestamp'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -204,7 +201,6 @@ describe.skip(' create route', () => { find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox form.setInputValue('watchTimeFieldSelect', '@timestamp'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -224,7 +220,6 @@ describe.skip(' create route', () => { // Provide valid value form.setInputValue('watchThresholdInput', '0'); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -244,7 +239,6 @@ describe.skip(' create route', () => { find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -270,7 +264,6 @@ describe.skip(' create route', () => { // Next, provide valid field and verify form.setInputValue('loggingTextInput', LOGGING_MESSAGE); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); @@ -339,7 +332,6 @@ describe.skip(' create route', () => { // Next, provide valid field and verify form.setInputValue('indexInput', INDEX); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); @@ -400,7 +392,6 @@ describe.skip(' create route', () => { form.setInputValue('slackMessageTextarea', SLACK_MESSAGE); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); @@ -471,7 +462,6 @@ describe.skip(' create route', () => { form.setInputValue('emailSubjectInput', EMAIL_SUBJECT); form.setInputValue('emailBodyInput', EMAIL_BODY); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); @@ -560,7 +550,6 @@ describe.skip(' create route', () => { form.setInputValue('webhookUsernameInput', USERNAME); form.setInputValue('webhookPasswordInput', PASSWORD); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); @@ -647,7 +636,6 @@ describe.skip(' create route', () => { form.setInputValue('jiraIssueTypeInput', ISSUE_TYPE); form.setInputValue('jiraSummaryInput', SUMMARY); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); @@ -726,7 +714,6 @@ describe.skip(' create route', () => { // Next, provide valid fields and verify form.setInputValue('pagerdutyDescriptionInput', DESCRIPTION); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSimulateButton(); await nextTick(); @@ -786,7 +773,6 @@ describe.skip(' create route', () => { find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox form.setInputValue('watchTimeFieldSelect', WATCH_TIME_FIELD); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index fb23a86980a33..fb9ad934249e9 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -63,7 +63,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -98,7 +97,6 @@ describe.skip('', () => { form.setInputValue('nameInput', EDITED_WATCH_NAME); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); @@ -155,7 +153,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { const { component } = testBed; await nextTick(); @@ -184,7 +181,6 @@ describe.skip('', () => { form.setInputValue('nameInput', EDITED_WATCH_NAME); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { actions.clickSubmitButton(); await nextTick(); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts index ebd6628c9c083..bc2eadb7d9be9 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts @@ -49,7 +49,6 @@ describe.skip('', () => { test('should display an empty prompt', async () => { const { component, exists } = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); component.update(); @@ -86,7 +85,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -219,7 +217,6 @@ describe.skip('', () => { }, }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { confirmButton!.click(); await nextTick(); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts index 3c01a5e007c2a..e12acd2e32ccf 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts @@ -60,7 +60,6 @@ describe.skip('', () => { testBed = await setup(); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { await nextTick(); testBed.component.update(); @@ -180,7 +179,6 @@ describe.skip('', () => { }, }); - // @ts-ignore (remove when react 16.9.0 is released) await act(async () => { confirmButton!.click(); await nextTick(); diff --git a/x-pack/package.json b/x-pack/package.json index c5114500c6f61..d5cb5abc1bb7b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -42,6 +42,8 @@ "@storybook/addon-storyshots": "^5.2.6", "@storybook/react": "^5.2.6", "@storybook/theming": "^5.2.6", + "@testing-library/react": "^9.3.2", + "@testing-library/jest-dom": "4.2.0", "@types/angular": "^1.6.56", "@types/archiver": "^3.0.0", "@types/base64-js": "^1.2.5", @@ -65,7 +67,7 @@ "@types/gulp": "^4.0.6", "@types/hapi__wreck": "^15.0.1", "@types/history": "^4.7.3", - "@types/jest": "^24.0.18", + "@types/jest": "24.0.19", "@types/joi": "^13.4.2", "@types/js-yaml": "^3.11.1", "@types/jsdom": "^12.2.4", @@ -86,13 +88,13 @@ "@types/prop-types": "^15.5.3", "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", - "@types/react": "^16.8.0", - "@types/react-dom": "^16.8.0", + "@types/react": "^16.9.11", + "@types/react-dom": "^16.9.4", "@types/react-redux": "^6.0.6", "@types/react-resize-detector": "^4.0.1", "@types/react-router-dom": "^4.3.1", "@types/react-sticky": "^6.0.3", - "@types/react-test-renderer": "^16.8.3", + "@types/react-test-renderer": "^16.9.1", "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^0.3.0", "@types/redux-actions": "^2.2.1", @@ -136,7 +138,7 @@ "jest": "^24.9.0", "jest-cli": "^24.9.0", "jest-styled-components": "^7.0.0-beta.2", - "jsdom": "^12.2.0", + "jsdom": "^15.2.1", "madge": "3.4.4", "marge": "^1.0.1", "mocha": "^6.2.2", @@ -152,9 +154,7 @@ "pixelmatch": "4.0.2", "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", - "react-hooks-testing-library": "^0.3.8", - "react-test-renderer": "^16.8.6", - "react-testing-library": "^6.0.0", + "react-test-renderer": "^16.12.0", "sass-loader": "^7.3.1", "sass-resources-loader": "^2.0.1", "simple-git": "1.116.0", @@ -293,18 +293,18 @@ "puid": "1.0.7", "puppeteer-core": "^1.19.0", "raw-loader": "3.1.0", - "react": "^16.8.6", + "react": "^16.12.0", "react-apollo": "^2.1.4", "react-beautiful-dnd": "^8.0.7", "react-datetime": "^2.14.0", - "react-dom": "^16.8.6", + "react-dom": "^16.12.0", "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", "react-markdown": "^3.4.1", "react-moment-proptypes": "^1.7.0", "react-monaco-editor": "~0.27.0", "react-portal": "^3.2.0", - "react-redux": "^5.1.1", + "react-redux": "^5.1.2", "react-resize-detector": "^4.2.0", "react-reverse-portal": "^1.0.4", "react-router-dom": "^4.3.1", diff --git a/x-pack/test_utils/testbed/mount_component.tsx b/x-pack/test_utils/testbed/mount_component.tsx index 4984ccca7cef9..2e8e2d9afbabc 100644 --- a/x-pack/test_utils/testbed/mount_component.tsx +++ b/x-pack/test_utils/testbed/mount_component.tsx @@ -48,17 +48,8 @@ export const mountComponentSync = (config: Config): ReactWrapper => { export const mountComponentAsync = async (config: Config): Promise => { const Comp = getCompFromConfig(config); - /** - * In order for hooks with effects to work in our tests - * we need to wrap the mounting under the new act "async" - * that ships with React 16.9.0 - * - * https://github.com/facebook/react/pull/14853 - * https://github.com/threepointone/react-act-examples/blob/master/sync.md - */ let component: ReactWrapper; - // @ts-ignore await act(async () => { component = mountWithIntl(); }); diff --git a/yarn.lock b/yarn.lock index e12a0eb46c6cc..7da65ebf8bc81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1005,20 +1005,27 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@7.5.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5": +"@babel/runtime@7.5.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": +"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.5.1", "@babel/runtime@^7.5.4", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2": + version "7.7.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.4.tgz#b23a856751e4bf099262f867767889c0e3fe175b" + integrity sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" @@ -2873,6 +2880,50 @@ "@svgr/plugin-svgo" "^4.2.0" loader-utils "^1.2.3" +"@testing-library/dom@^6.3.0": + version "6.10.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-6.10.1.tgz#da5bf5065d3f9e484aef4cc495f4e1a5bea6df2e" + integrity sha512-5BPKxaO+zSJDUbVZBRNf9KrmDkm/EcjjaHSg3F9+031VZyPACKXlwLBjVzZxheunT9m72DoIq7WvyE457/Xweg== + dependencies: + "@babel/runtime" "^7.6.2" + "@sheerun/mutationobserver-shim" "^0.3.2" + "@types/testing-library__dom" "^6.0.0" + aria-query "3.0.0" + pretty-format "^24.9.0" + wait-for-expect "^3.0.0" + +"@testing-library/jest-dom@4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-4.2.0.tgz#32f8df3a78511b347d39374ea89dc8e0a1c2fb69" + integrity sha512-H61OmRhGPWLrj9emyISx0qjp8jvC9RWyRniuLAq75Ny5XfPiOvWfnY3Wm2Tf0HXusX+PG40I94Gw792IAtSKKg== + dependencies: + "@babel/runtime" "^7.5.1" + chalk "^2.4.1" + css "^2.2.3" + css.escape "^1.5.1" + jest-diff "^24.0.0" + jest-matcher-utils "^24.0.0" + lodash "^4.17.11" + pretty-format "^24.0.0" + redent "^3.0.0" + +"@testing-library/react-hooks@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.2.1.tgz#19b6caa048ef15faa69d439c469033873ea01294" + integrity sha512-1OB6Ksvlk6BCJA1xpj8/WWz0XVd1qRcgqdaFAq+xeC6l61Ucj0P6QpA5u+Db/x9gU4DCX8ziR5b66Mlfg0M2RA== + dependencies: + "@babel/runtime" "^7.5.4" + "@types/testing-library__react-hooks" "^3.0.0" + +"@testing-library/react@^9.3.2": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-9.3.2.tgz#418000daa980dafd2d9420cc733d661daece9aa0" + integrity sha512-J6ftWtm218tOLS175MF9eWCxGp+X+cUXCpkPIin8KAXWtyZbr9CbqJ8M8QNd6spZxJDAGlw+leLG4MJWLlqVgg== + dependencies: + "@babel/runtime" "^7.6.0" + "@testing-library/dom" "^6.3.0" + "@types/testing-library__react" "^9.1.0" + "@turf/bbox@6.x": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.0.1.tgz#b966075771475940ee1c16be2a12cf389e6e923a" @@ -3424,14 +3475,16 @@ "@types/istanbul-lib-report" "*" "@types/jest-diff@*": - version "20.0.1" - resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" - integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== + version "24.3.0" + resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-24.3.0.tgz#29e237a3d954babfe6e23cc59b57ecd8ca8d858d" + integrity sha512-vx1CRDeDUwQ0Pc7v+hS61O1ETA81kD04IMEC0hS1kPyVtHDdZrokAvpF7MT9VI/fVSzicelUZNCepDvhRV1PeA== + dependencies: + jest-diff "*" -"@types/jest@^24.0.18": - version "24.0.18" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.18.tgz#9c7858d450c59e2164a8a9df0905fc5091944498" - integrity sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ== +"@types/jest@24.0.19": + version "24.0.19" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.19.tgz#f7036058d2a5844fe922609187c0ad8be430aff5" + integrity sha512-YYiqfSjocv7lk5H/T+v5MjATYjaTMsUkbDnjGqSMoO88jWdtJXJV4ST/7DKZcoMHMBvB2SeSfyOzZfkxXHR5xg== dependencies: "@types/jest-diff" "*" @@ -3764,10 +3817,10 @@ dependencies: "@types/react" "*" -"@types/react-dom@^16.8.0": - version "16.8.2" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.2.tgz#9bd7d33f908b243ff0692846ef36c81d4941ad12" - integrity sha512-MX7n1wq3G/De15RGAAqnmidzhr2Y9O/ClxPxyqaNg96pGyeXUYPSvujgzEVpLo9oIP4Wn1UETl+rxTN02KEpBw== +"@types/react-dom@*", "@types/react-dom@^16.9.4": + version "16.9.4" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.4.tgz#0b58df09a60961dcb77f62d4f1832427513420df" + integrity sha512-fya9xteU/n90tda0s+FtN5Ym4tbgxpq/hb/Af24dvs6uYnYn+fspaxw5USlw0R8apDNwxsqumdRoCoKitckQqw== dependencies: "@types/react" "*" @@ -3837,7 +3890,7 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@^16.8.3": +"@types/react-test-renderer@*", "@types/react-test-renderer@^16.9.1": version "16.9.1" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4" integrity sha512-nCXQokZN1jp+QkoDNmDZwoWpKY8HDczqevIDO4Uv9/s9rbGPbSpy8Uaxa5ixHKkcm/Wt0Y9C3wCxZivh4Al+rQ== @@ -3859,10 +3912,10 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react@*", "@types/react@16.8.3", "@types/react@^16.8.0", "@types/react@^16.8.23": - version "16.8.3" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.3.tgz#7b67956f682bea30a5a09b3242c0784ff196c848" - integrity sha512-PjPocAxL9SNLjYMP4dfOShW/rj9FDBJGu3JFRt0zEYf77xfihB6fq8zfDpMrV6s82KnAi7F1OEe5OsQX25Ybdw== +"@types/react@*", "@types/react@^16.8.23", "@types/react@^16.9.11", "@types/react@^16.9.13": + version "16.9.13" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.13.tgz#b3ea5dd443f4a680599e2abba8cc66f5e1ce0059" + integrity sha512-LikzRslbiufJYHyzbHSW0GrAiff8QYLMBFeZmSxzCYGXKxi8m/1PHX+rsVOwhr7mJNq+VIu2Dhf7U6mjFERK6w== dependencies: "@types/prop-types" "*" csstype "^2.2.0" @@ -4018,6 +4071,29 @@ resolved "https://registry.yarnpkg.com/@types/tempy/-/tempy-0.2.0.tgz#8b7a93f6912aef25cc0b8d8a80ff974151478685" integrity sha512-YaX74QljqR45Xu7dd22wMvzTS+ItUiSyDl9XJl6WTgYNE09r2TF+mV2FDjWRM5Sdzf9C9dXRTUdz9J5SoEYxXg== +"@types/testing-library__dom@*", "@types/testing-library__dom@^6.0.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-6.10.0.tgz#590d76e3875a7c536dc744eb530cbf51b6483404" + integrity sha512-mL/GMlyQxiZplbUuFNwA0vAI3k3uJNSf6slr5AVve9TXmfLfyefNT0uHHnxwdYuPMxYD5gI/+dgAvc/5opW9JQ== + dependencies: + pretty-format "^24.3.0" + +"@types/testing-library__react-hooks@^3.0.0", "@types/testing-library__react-hooks@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.1.0.tgz#04d174ce767fbcce3ccb5021d7f156e1b06008a9" + integrity sha512-QJc1sgH9DD6jbfybzugnP0sY8wPzzIq8sHDBuThzCr2ZEbyHIaAvN9ytx/tHzcWL5MqmeZJqiUm/GsythaGx3g== + dependencies: + "@types/react" "*" + "@types/react-test-renderer" "*" + +"@types/testing-library__react@^9.1.0", "@types/testing-library__react@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@types/testing-library__react/-/testing-library__react-9.1.2.tgz#e33af9124c60a010fc03a34eff8f8a34a75c4351" + integrity sha512-CYaMqrswQ+cJACy268jsLAw355DZtPZGt3Jwmmotlcu8O/tkoXBI6AeZ84oZBJsIsesozPKzWzmv/0TIU+1E9Q== + dependencies: + "@types/react-dom" "*" + "@types/testing-library__dom" "*" + "@types/tinycolor2@^1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf" @@ -4502,7 +4578,7 @@ acorn-globals@^4.0.0: acorn "^6.0.1" acorn-walk "^6.0.1" -acorn-globals@^4.3.0: +acorn-globals@^4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== @@ -4541,7 +4617,7 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.0.0.tgz#c8ba6f0f1aac4b0a9e32d1f0af12be769528f36b" integrity sha512-7Bv1We7ZGuU79zZbb6rRqcpxo3OY+zrdtloZWoyD8fmGX+FeXRjE+iuGkZjSXLVovLzrsvMGMy0EkwA0E0umxg== -acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.5.0: +acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.2.1, acorn@^5.5.0: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== @@ -4561,12 +4637,12 @@ acorn@^6.0.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== -acorn@^6.0.2, acorn@^6.0.5, acorn@^6.2.1: +acorn@^6.0.5, acorn@^6.2.1: version "6.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== -acorn@^7.0.0: +acorn@^7.0.0, acorn@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== @@ -5312,7 +5388,7 @@ argv-split@^2.0.1: resolved "https://registry.yarnpkg.com/argv-split/-/argv-split-2.0.1.tgz#be264117790dbd5ccd63ec3f449a1804814ac4c5" integrity sha1-viZBF3kNvVzNY+w/RJoYBIFKxMU= -aria-query@^3.0.0: +aria-query@3.0.0, aria-query@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= @@ -5646,6 +5722,11 @@ async-foreach@^0.1.3: resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= +async-limiter@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" @@ -6374,6 +6455,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base62@^1.1.0: + version "1.2.8" + resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" + integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== + base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -8227,6 +8313,11 @@ commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0, comm resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@^2.5.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^2.8.1: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -8256,6 +8347,21 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= +commoner@^0.10.1: + version "0.10.8" + resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" + integrity sha1-NPw2cs0kOT6LtH5wyqApOBH08sU= + dependencies: + commander "^2.5.0" + detective "^4.3.1" + glob "^5.0.15" + graceful-fs "^4.1.2" + iconv-lite "^0.4.5" + mkdirp "^0.5.0" + private "^0.1.6" + q "^1.1.2" + recast "^0.11.17" + compare-versions@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" @@ -9103,7 +9209,12 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" integrity sha1-uANhcMefB6kP8vFuIihAJ6JDhIs= -cssom@^0.3.4: +cssom@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@~0.3.6: version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== @@ -9115,12 +9226,12 @@ cssom@^0.3.4: dependencies: cssom "0.3.x" -cssstyle@^1.1.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" - integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== +cssstyle@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.0.0.tgz#911f0fe25532db4f5d44afc83f89cc4b82c97fe3" + integrity sha512-QXSAu2WBsSRXCPjvI43Y40m6fMevvyRm8JVAuF9ksQz5jha4pWP1wpaK7Yu5oLFc6+XAY+hj8YhefyXcBB53gg== dependencies: - cssom "0.3.x" + cssom "~0.3.6" csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7: version "2.6.7" @@ -9502,13 +9613,13 @@ dashify@^0.1.0: resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea" integrity sha1-EH2vnMpeMm4wqLOf+lBItmhJIuo= -data-urls@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.0.1.tgz#d416ac3896918f29ca84d81085bc3705834da579" - integrity sha512-0HdcMZzK6ubMUnsMmQmG0AcLQPvbvb47R0+7CCZQCYgcd8OUWG91CG7sM6GoXgjz+WLl4ArFzHtBMy/QqSF4eg== +data-urls@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== dependencies: abab "^2.0.0" - whatwg-mimetype "^2.1.0" + whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" date-fns@^1.27.2: @@ -9795,7 +9906,7 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defined@~1.0.0: +defined@^1.0.0, defined@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= @@ -10044,6 +10155,14 @@ detective-typescript@^5.1.1: node-source-walk "^4.2.0" typescript "^3.4.5" +detective@^4.3.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" + integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig== + dependencies: + acorn "^5.2.1" + defined "^1.0.0" + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -10236,26 +10355,6 @@ dom-serializer@0, dom-serializer@~0.1.0, dom-serializer@~0.1.1: domelementtype "^1.3.0" entities "^1.1.1" -dom-testing-library@^3.13.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.17.1.tgz#3291bc3cf68c555ba5e663697ee77d604aaa122b" - integrity sha512-SbkaRfQvuLjnv+xFgSo/cmKoN9tjBL6Rh1f3nQH9jnjUe5q+keRwacYSi3uSpcB4D1K768iavCayKH3ZN9ea+g== - dependencies: - "@babel/runtime" "^7.3.4" - "@sheerun/mutationobserver-shim" "^0.3.2" - pretty-format "^24.0.0" - wait-for-expect "^1.1.0" - -dom-testing-library@^3.18.2: - version "3.18.2" - resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.18.2.tgz#07d65166743ad3299b7bee5b488e9622c31241bc" - integrity sha512-+nYUgGhHarrCY8kLVmyHlgM+IGwBXXrYsWIJB6vpAx2ne9WFgKfwMGcOkkTKQhuAro0sP6RIuRGfm5NF3+ccmQ== - dependencies: - "@babel/runtime" "^7.3.4" - "@sheerun/mutationobserver-shim" "^0.3.2" - pretty-format "^24.5.0" - wait-for-expect "^1.1.0" - dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -10722,6 +10821,14 @@ env-variable@0.0.x: resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88" integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA== +envify@^3.0.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/envify/-/envify-3.4.1.tgz#d7122329e8df1688ba771b12501917c9ce5cbce8" + integrity sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg= + dependencies: + jstransform "^11.0.3" + through "~2.3.4" + enzyme-adapter-react-16@^1.15.1: version "1.15.1" resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5" @@ -10984,10 +11091,10 @@ escodegen@1.8.x: optionalDependencies: source-map "~0.2.0" -escodegen@^1.11.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" - integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw== +escodegen@^1.11.1, escodegen@^1.8.1: + version "1.12.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541" + integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg== dependencies: esprima "^3.1.3" estraverse "^4.2.0" @@ -11008,18 +11115,6 @@ escodegen@^1.8.0: optionalDependencies: source-map "~0.6.1" -escodegen@^1.8.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541" - integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg== - dependencies: - esprima "^3.1.3" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - escodegen@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852" @@ -11413,6 +11508,11 @@ espree@^6.1.1: acorn-jsx "^5.0.2" eslint-visitor-keys "^1.1.0" +esprima-fb@^15001.1.0-dev-harmony-fb: + version "15001.1.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901" + integrity sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE= + esprima@2.7.x, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -12087,6 +12187,17 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +fbjs@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.6.1.tgz#9636b7705f5ba9684d44b72f78321254afc860f7" + integrity sha1-lja3cF9bqWhNRLcveDISVK/IYPc= + dependencies: + core-js "^1.0.0" + loose-envify "^1.0.0" + promise "^7.0.3" + ua-parser-js "^0.7.9" + whatwg-fetch "^0.9.0" + fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.16: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" @@ -14667,7 +14778,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0, hoist-non-react- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -14957,7 +15068,7 @@ icalendar@0.7.1: resolved "https://registry.yarnpkg.com/icalendar/-/icalendar-0.7.1.tgz#d0d3486795f8f1c5cf4f8cafac081b4b4e7a32ae" integrity sha1-0NNIZ5X48cXPT4yvrAgbS056Mq4= -iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -15176,6 +15287,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -16490,6 +16606,16 @@ jest-config@^24.9.0: pretty-format "^24.9.0" realpath-native "^1.1.0" +jest-diff@*, jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + jest-diff@^24.0.0: version "24.0.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.0.0.tgz#a3e5f573dbac482f7d9513ac9cfa21644d3d6b34" @@ -16500,16 +16626,6 @@ jest-diff@^24.0.0: jest-get-type "^24.0.0" pretty-format "^24.0.0" -jest-diff@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" - integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== - dependencies: - chalk "^2.0.1" - diff-sequences "^24.9.0" - jest-get-type "^24.9.0" - pretty-format "^24.9.0" - jest-docblock@^24.3.0: version "24.3.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd" @@ -16517,20 +16633,6 @@ jest-docblock@^24.3.0: dependencies: detect-newline "^2.1.0" -jest-dom@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jest-dom/-/jest-dom-3.5.0.tgz#715908b545c0d66a0eba9d21fc59357fac024f43" - integrity sha512-xHnP3Qo/29oLAo2iixaZsoDrm3XKSVrMH5Wf2ZEiLychJQBTNzOeVMPxrCygCgJiyQMbnymXltme8bPzuiGOIA== - dependencies: - chalk "^2.4.1" - css "^2.2.3" - css.escape "^1.5.1" - jest-diff "^24.0.0" - jest-matcher-utils "^24.0.0" - lodash "^4.17.11" - pretty-format "^24.0.0" - redent "^2.0.0" - jest-each@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" @@ -16977,35 +17079,36 @@ jsdom@^11.5.1: whatwg-url "^6.3.0" xml-name-validator "^2.0.1" -jsdom@^12.2.0: - version "12.2.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-12.2.0.tgz#7cf3f5b5eafd47f8f09ca52315d367ff6e95de23" - integrity sha512-QPOggIJ8fquWPLaYYMoh+zqUmdphDtu1ju0QGTitZT1Yd8I5qenPpXM1etzUegu3MjVp8XPzgZxdn8Yj7e40ig== +jsdom@^15.2.1: + version "15.2.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5" + integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g== dependencies: abab "^2.0.0" - acorn "^6.0.2" - acorn-globals "^4.3.0" + acorn "^7.1.0" + acorn-globals "^4.3.2" array-equal "^1.0.0" - cssom "^0.3.4" - cssstyle "^1.1.1" - data-urls "^1.0.1" + cssom "^0.4.1" + cssstyle "^2.0.0" + data-urls "^1.1.0" domexception "^1.0.1" - escodegen "^1.11.0" + escodegen "^1.11.1" html-encoding-sniffer "^1.0.2" - nwsapi "^2.0.9" + nwsapi "^2.2.0" parse5 "5.1.0" pn "^1.1.0" request "^2.88.0" - request-promise-native "^1.0.5" - saxes "^3.1.3" + request-promise-native "^1.0.7" + saxes "^3.1.9" symbol-tree "^3.2.2" - tough-cookie "^2.4.3" + tough-cookie "^3.0.1" w3c-hr-time "^1.0.1" + w3c-xmlserializer "^1.1.2" webidl-conversions "^4.0.2" whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.2.0" + whatwg-mimetype "^2.3.0" whatwg-url "^7.0.0" - ws "^6.1.0" + ws "^7.0.0" xml-name-validator "^3.0.0" jsesc@^1.3.0: @@ -17204,6 +17307,17 @@ jssha@^2.1.0: resolved "https://registry.yarnpkg.com/jssha/-/jssha-2.3.1.tgz#147b2125369035ca4b2f7d210dc539f009b3de9a" integrity sha1-FHshJTaQNcpLL30hDcU58Amz3po= +jstransform@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223" + integrity sha1-CaeJk+CuTU70SH9hVakfYZDLQiM= + dependencies: + base62 "^1.1.0" + commoner "^0.10.1" + esprima-fb "^15001.1.0-dev-harmony-fb" + object-assign "^2.0.0" + source-map "^0.4.2" + jstransformer-ejs@^0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/jstransformer-ejs/-/jstransformer-ejs-0.0.3.tgz#04d9201469274fcf260f1e7efd732d487fa234b6" @@ -19076,6 +19190,11 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" +min-indent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" + integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= + mini-css-extract-plugin@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" @@ -20201,10 +20320,10 @@ nwmatcher@^1.4.3: resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e" integrity sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ== -nwsapi@^2.0.9: - version "2.1.4" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" - integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== +nwsapi@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== nyc@^14.1.1: version "14.1.1" @@ -21813,7 +21932,7 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^24.0.0, pretty-format@^24.5.0: +pretty-format@^24.0.0: version "24.8.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" integrity sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw== @@ -21823,7 +21942,7 @@ pretty-format@^24.0.0, pretty-format@^24.5.0: ansi-styles "^3.2.0" react-is "^16.8.4" -pretty-format@^24.9.0: +pretty-format@^24.3.0, pretty-format@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== @@ -21906,7 +22025,7 @@ promise.prototype.finally@^3.1.0: es-abstract "^1.9.0" function-bind "^1.1.1" -promise@^7.0.1, promise@^7.1.1: +promise@^7.0.1, promise@^7.0.3, promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== @@ -22038,6 +22157,11 @@ psl@^1.1.24: resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ== +psl@^1.1.28: + version "1.4.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" + integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== + public-encrypt@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" @@ -22215,7 +22339,7 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@2.x.x, punycode@^2.1.0: +punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -22538,14 +22662,6 @@ react-addons-create-fragment@^15.6.2: loose-envify "^1.3.1" object-assign "^4.1.0" -react-addons-shallow-compare@15.6.2: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz#198a00b91fc37623db64a28fd17b596ba362702f" - integrity sha1-GYoAuR/DdiPbZKKP0XtZa6NicC8= - dependencies: - fbjs "^0.8.4" - object-assign "^4.1.0" - react-apollo@^2.1.4: version "2.1.8" resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.1.8.tgz#ebac0d9bee0f0906df3ce29207f94df337009887" @@ -22708,15 +22824,15 @@ react-docgen@^4.1.0: node-dir "^0.1.10" recast "^0.17.3" -react-dom@16.8.6, react-dom@^16.8.3, react-dom@^16.8.5, react-dom@^16.8.6: - version "16.8.6" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" - integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== +react-dom@^16.12.0, react-dom@^16.8.3, react-dom@^16.8.5: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" + integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.6" + scheduler "^0.18.0" react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3": version "3.0.5" @@ -22802,21 +22918,6 @@ react-helmet-async@^1.0.2: react-fast-compare "2.0.4" shallowequal "1.1.0" -react-hooks-testing-library@^0.3.8: - version "0.3.8" - resolved "https://registry.yarnpkg.com/react-hooks-testing-library/-/react-hooks-testing-library-0.3.8.tgz#717595ed7be500023963dd502f188aa932bf70f0" - integrity sha512-YFnyd2jH2voikSBGufqhprnxMTHgosOHlO5EXhuQycWxfeTCIiw/17aiYbpvRRDRB/0j8QvI/jHXMNVBKw7WzA== - dependencies: - "@babel/runtime" "^7.4.2" - react-testing-library "^6.0.2" - -react-hooks-testing-library@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/react-hooks-testing-library/-/react-hooks-testing-library-0.5.0.tgz#571af3522f88ea4ac23c634fb4deff84873f2bc2" - integrity sha512-qX4SA28pcCCf1Q23Gtl1VKqQk26pSPTEsdLtfJanDqm4oacT5wadL+e2Xypk/H+AOXN5kdZrWmXkt+hAaiNHgg== - dependencies: - "@babel/runtime" "^7.4.2" - react-hotkeys@2.0.0-pre4: version "2.0.0-pre4" resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0-pre4.tgz#a1c248a51bdba4282c36bf3204f80d58abc73333" @@ -23023,7 +23124,7 @@ react-reconciler@^0.20.1: prop-types "^15.6.2" scheduler "^0.13.6" -react-redux@^5.0.6, react-redux@^5.0.7: +react-redux@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" integrity sha512-5VI8EV5hdgNgyjfmWzBbdrqUkrVRKlyTKk1sGH3jzM2M2Mhj/seQgPXaz6gVAj2lz/nz688AdTqMO18Lr24Zhg== @@ -23035,13 +23136,13 @@ react-redux@^5.0.6, react-redux@^5.0.7: loose-envify "^1.1.0" prop-types "^15.6.0" -react-redux@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.1.tgz#88e368682c7fa80e34e055cd7ac56f5936b0f52f" - integrity sha512-LE7Ned+cv5qe7tMV5BPYkGQ5Lpg8gzgItK07c67yHvJ8t0iaD9kPFPAli/mYkiyJYrs2pJgExR2ZgsGqlrOApg== +react-redux@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" + integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== dependencies: "@babel/runtime" "^7.1.2" - hoist-non-react-statics "^3.1.0" + hoist-non-react-statics "^3.3.0" invariant "^2.2.4" loose-envify "^1.1.0" prop-types "^15.6.1" @@ -23208,31 +23309,15 @@ react-syntax-highlighter@^8.0.1: prismjs "^1.8.4" refractor "^2.4.1" -react-test-renderer@16.8.6, react-test-renderer@^16.0.0-0, react-test-renderer@^16.8.6: - version "16.8.6" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1" - integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw== +react-test-renderer@^16.0.0-0, react-test-renderer@^16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.12.0.tgz#11417ffda579306d4e841a794d32140f3da1b43f" + integrity sha512-Vj/teSqt2oayaWxkbhQ6gKis+t5JrknXfPVo+aIJ8QwYAqMPH77uptOdrlphyxl8eQI/rtkOYg86i/UWkpFu0w== dependencies: object-assign "^4.1.1" prop-types "^15.6.2" react-is "^16.8.6" - scheduler "^0.13.6" - -react-testing-library@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.0.tgz#81edfcfae8a795525f48685be9bf561df45bb35d" - integrity sha512-h0h+YLe4KWptK6HxOMnoNN4ngu3W8isrwDmHjPC5gxc+nOZOCurOvbKVYCvvuAw91jdO7VZSm/5KR7TxKnz0qA== - dependencies: - "@babel/runtime" "^7.3.1" - dom-testing-library "^3.13.1" - -react-testing-library@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.3.tgz#8b5d276a353c17ce4f7486015bb7a1c8827c442c" - integrity sha512-tN0A6nywSOoL8kriqru3rSdw31PxuquL7xnW6xBI0aTNw0VO3kZQtaEa0npUH9dX0MIsSunB0nbElRrc4VtAzw== - dependencies: - "@babel/runtime" "^7.4.2" - dom-testing-library "^3.18.2" + scheduler "^0.18.0" react-textarea-autosize@^7.1.0: version "7.1.0" @@ -23305,7 +23390,24 @@ react-visibility-sensor@^5.1.1: dependencies: prop-types "^15.7.2" -react@16.8.6, react@^0.14.0, react@^16.8.3, react@^16.8.5, react@^16.8.6: +react@^0.14.0: + version "0.14.9" + resolved "https://registry.yarnpkg.com/react/-/react-0.14.9.tgz#9110a6497c49d44ba1c0edd317aec29c2e0d91d1" + integrity sha1-kRCmSXxJ1EuhwO3TF67CnC4NkdE= + dependencies: + envify "^3.0.0" + fbjs "^0.6.1" + +react@^16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" + integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + +react@^16.8.3, react@^16.8.5: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== @@ -23538,6 +23640,16 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +recast@^0.11.17, recast@~0.11.12: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + recast@^0.14.7: version "0.14.7" resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" @@ -23558,16 +23670,6 @@ recast@^0.17.3: private "^0.1.8" source-map "~0.6.1" -recast@~0.11.12: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -23608,6 +23710,14 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + redeyed@~0.4.0: version "0.4.4" resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-0.4.4.tgz#37e990a6f2b21b2a11c2e6a48fd4135698cba97f" @@ -24011,7 +24121,14 @@ request-promise-core@1.1.2: dependencies: lodash "^4.17.11" -request-promise-native@^1.0.3, request-promise-native@^1.0.5: +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== + dependencies: + lodash "^4.17.15" + +request-promise-native@^1.0.3: version "1.0.5" resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU= @@ -24020,6 +24137,15 @@ request-promise-native@^1.0.3, request-promise-native@^1.0.5: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" +request-promise-native@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" + integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== + dependencies: + request-promise-core "1.1.3" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + request-promise@^4.2.2: version "4.2.4" resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.4.tgz#1c5ed0d71441e38ad58c7ce4ea4ea5b06d54b310" @@ -24754,7 +24880,7 @@ sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -saxes@^3.1.3: +saxes@^3.1.9: version "3.1.11" resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g== @@ -24769,6 +24895,14 @@ scheduler@^0.13.3, scheduler@^0.13.6: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4" + integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" @@ -26203,6 +26337,13 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -27140,7 +27281,7 @@ topojson-client@3.0.0, topojson-client@^3.0.0: dependencies: commander "2" -tough-cookie@>=2.3.3, tough-cookie@^2.0.0, tough-cookie@^2.3.3, tough-cookie@^2.4.3, tough-cookie@~2.4.3: +tough-cookie@>=2.3.3, tough-cookie@^2.0.0, tough-cookie@^2.3.3, tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== @@ -27148,6 +27289,15 @@ tough-cookie@>=2.3.3, tough-cookie@^2.0.0, tough-cookie@^2.3.3, tough-cookie@^2. psl "^1.1.24" punycode "^1.4.1" +tough-cookie@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" + integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== + dependencies: + ip-regex "^2.1.0" + psl "^1.1.28" + punycode "^2.1.1" + tough-cookie@~2.3.0, tough-cookie@~2.3.3: version "2.3.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" @@ -29167,10 +29317,19 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" -wait-for-expect@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.0.tgz#6607375c3f79d32add35cd2c87ce13f351a3d453" - integrity sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg== +w3c-xmlserializer@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794" + integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg== + dependencies: + domexception "^1.0.1" + webidl-conversions "^4.0.2" + xml-name-validator "^3.0.0" + +wait-for-expect@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.1.tgz#ec204a76b0038f17711e575720aaf28505ac7185" + integrity sha512-3Ha7lu+zshEG/CeHdcpmQsZnnZpPj/UsG3DuKO8FskjuDbkx3jE3845H+CuwZjA2YWYDfKMU2KhnCaXMLd3wVw== walk@2.3.x: version "2.3.9" @@ -29532,12 +29691,12 @@ whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== -whatwg-mimetype@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171" - integrity sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw== +whatwg-fetch@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0" + integrity sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA= -whatwg-mimetype@^2.2.0: +whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== @@ -29883,6 +30042,13 @@ ws@^6.2.1: dependencies: async-limiter "~1.0.0" +ws@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.0.tgz#422eda8c02a4b5dba7744ba66eebbd84bcef0ec7" + integrity sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg== + dependencies: + async-limiter "^1.0.0" + ws@~3.3.1: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" From a35a6cac39594093b0d999aefb17c601b0c5d9b0 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 28 Nov 2019 11:33:22 +0100 Subject: [PATCH 126/128] fixes timeline data providers tests (#51862) --- .../plugins/siem/cypress/integration/lib/hosts/selectors.ts | 2 +- .../siem/public/components/drag_and_drop/droppable_wrapper.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts index ec6c64e116b24..1c900944752c4 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts @@ -8,7 +8,7 @@ export const ALL_HOSTS_WIDGET = '[data-test-subj="table-allHosts-loading-false"]'; /** A single draggable host in the `All Hosts` widget on the `Hosts` page */ -export const ALL_HOSTS_WIDGET_HOST = '[data-react-beautiful-dnd-drag-handle]'; +export const ALL_HOSTS_WIDGET_HOST = '[data-test-subj="draggable-content-host.name"]'; /** All the draggable hosts in the `All Hosts` widget on the `Hosts` page */ export const ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS = `${ALL_HOSTS_WIDGET} ${ALL_HOSTS_WIDGET_HOST}`; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index b7ceac77aa1f1..3f789a39832f1 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -40,7 +40,7 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string background-color: ${rgba(props.theme.eui.euiColorSuccess, 0.3)}; } > div.timeline-drop-area-empty { - color: ${props.theme.eui.euiColorSuccess} + color: ${props.theme.eui.euiColorSuccess}; background-color: ${rgba(props.theme.eui.euiColorSuccess, 0.2)}; & .euiTextColor--subdued { From cd109fa7c7593cf174b74474734ec8fabf20d0f5 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 28 Nov 2019 13:28:32 +0100 Subject: [PATCH 127/128] [Discover] Improve Percy functional tests (#51699) * Implement new wait for chart rendered function * Add findByCssSelector to ensure the charts have been rendered --- .../tests/discover/chart_visualization.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/visual_regression/tests/discover/chart_visualization.js b/test/visual_regression/tests/discover/chart_visualization.js index 540d95973b547..c90f29c66acb8 100644 --- a/test/visual_regression/tests/discover/chart_visualization.js +++ b/test/visual_regression/tests/discover/chart_visualization.js @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const visualTesting = getService('visualTesting'); + const find = getService('find'); const defaultSettings = { defaultIndex: 'logstash-*', 'discover:sampleSize': 1 @@ -48,10 +49,12 @@ export default function ({ getService, getPageObjects }) { describe('query', function () { this.tags(['skipFirefox']); + let renderCounter = 0; it('should show bars in the correct time zone', async function () { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -61,6 +64,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Hourly'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -70,6 +74,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Daily'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -79,6 +84,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Weekly'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -92,6 +98,7 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -101,6 +108,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Monthly'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -110,6 +118,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Yearly'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -119,6 +128,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Auto'); + await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); await visualTesting.snapshot({ show: ['discoverChart'], }); From af23f302c0ec213114dce829688fa8f0bf7a158e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 28 Nov 2019 08:59:00 -0500 Subject: [PATCH 128/128] Fix error returned when creating an alert with ES security disabled (#51639) * Fix error returned when creating an alert with ES security disabled * Add test to ensure error gets thrown when inner function throws --- .../server/lib/alerts_client_factory.test.ts | 24 +++++++++++++++++++ .../server/lib/alerts_client_factory.ts | 12 ++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts index 1063e20e4ba3b..a465aebc8bd86 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts @@ -93,6 +93,16 @@ test('createAPIKey() returns { created: false } when security is disabled', asyn expect(createAPIKeyResult).toEqual({ created: false }); }); +test('createAPIKey() returns { created: false } when security is enabled but ES security is disabled', async () => { + const factory = new AlertsClientFactory(alertsClientFactoryParams); + factory.create(KibanaRequest.from(fakeRequest), fakeRequest); + const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0]; + + securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce(null); + const createAPIKeyResult = await constructorCall.createAPIKey(); + expect(createAPIKeyResult).toEqual({ created: false }); +}); + test('createAPIKey() returns an API key when security is enabled', async () => { const factory = new AlertsClientFactory({ ...alertsClientFactoryParams, @@ -105,3 +115,17 @@ test('createAPIKey() returns an API key when security is enabled', async () => { const createAPIKeyResult = await constructorCall.createAPIKey(); expect(createAPIKeyResult).toEqual({ created: true, result: { api_key: '123', id: 'abc' } }); }); + +test('createAPIKey() throws when security plugin createAPIKey throws an error', async () => { + const factory = new AlertsClientFactory({ + ...alertsClientFactoryParams, + securityPluginSetup: securityPluginSetup as any, + }); + factory.create(KibanaRequest.from(fakeRequest), fakeRequest); + const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0]; + + securityPluginSetup.authc.createAPIKey.mockRejectedValueOnce(new Error('TLS disabled')); + await expect(constructorCall.createAPIKey()).rejects.toThrowErrorMatchingInlineSnapshot( + `"TLS disabled"` + ); +}); diff --git a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts index bacb346042187..b75d681b6586a 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts @@ -53,12 +53,16 @@ export class AlertsClientFactory { if (!securityPluginSetup) { return { created: false }; } + const createAPIKeyResult = await securityPluginSetup.authc.createAPIKey(request, { + name: `source: alerting, generated uuid: "${uuid.v4()}"`, + role_descriptors: {}, + }); + if (!createAPIKeyResult) { + return { created: false }; + } return { created: true, - result: (await securityPluginSetup.authc.createAPIKey(request, { - name: `source: alerting, generated uuid: "${uuid.v4()}"`, - role_descriptors: {}, - }))!, + result: createAPIKeyResult, }; }, });
    +
    - +
    - + {{ monitoringMain.instance }} + class="euiTab" + >{{ monitoringMain.instance }}

    _=E) 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ℑ8V=Es`xq4tL%904-zedzv7^kL#lWqL!4o@jL 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 F{|0f4x;y{? diff --git a/docs/user/reporting/images/share-button.png b/docs/user/reporting/images/share-button.png index 178ffa90d790bbab6a95019cfb3034afb002aaf5..46a4cce598119c449b26c92b26081af34e18c47c 100644 GIT binary patch literal 136368 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