From bd952721a4791da8cd7c8d9cfdfa1e6660cf9fa8 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Mon, 6 Jul 2020 21:02:26 +0300 Subject: [PATCH 01/57] Convert tag cloud tests to jest (#70066) * Convert tag cloud tests to jest * Add mocks to test_utils and remove tests from legacy * Revert changes made by accident * Update tag_cloud_visualization.test.js * Update tag_cloud.test.js * Update jsdom_svg_mocks.ts * Add restoring previous value to window.SVGElement.prototype.transform * Get rid of some deep imports * Reimport jsdom_svg_mocks functions from test_utils/public * Get rid of ExprVis by inlining some of its params to vis object Co-authored-by: Elastic Machine Co-authored-by: Alexey Antonov --- package.json | 3 +- .../vis_type_tagcloud/afterparamchange.png | Bin 11622 -> 0 bytes .../vis_type_tagcloud/afterresize.png | Bin 9012 -> 0 bytes .../__tests__/vis_type_tagcloud/basicdraw.png | Bin 12964 -> 0 bytes .../vis_type_tagcloud/simpleload.png | Bin 10359 -> 0 bytes .../tag_cloud_visualization.js | 202 --------------- .../__snapshots__/tag_cloud.test.js.snap | 3 + .../tag_cloud_visualization.test.js.snap | 7 + .../public/components/tag_cloud.test.js} | 231 ++++++++---------- .../tag_cloud_visualization.test.js | 176 +++++++++++++ src/test_utils/public/helpers/index.ts | 2 + .../public/helpers/jsdom_svg_mocks.ts | 57 +++++ src/test_utils/public/index.ts | 20 ++ yarn.lock | 25 ++ 14 files changed, 395 insertions(+), 331 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js create mode 100644 src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap create mode 100644 src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap rename src/{legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js => plugins/vis_type_tagcloud/public/components/tag_cloud.test.js} (72%) create mode 100644 src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js create mode 100644 src/test_utils/public/helpers/jsdom_svg_mocks.ts create mode 100644 src/test_utils/public/index.ts diff --git a/package.json b/package.json index 8e51f9207eaf18..2f6b643b026015 100644 --- a/package.json +++ b/package.json @@ -455,9 +455,10 @@ "is-path-inside": "^2.1.0", "istanbul-instrumenter-loader": "3.0.1", "jest": "^25.5.4", - "jest-environment-jsdom-thirteen": "^1.0.1", + "jest-canvas-mock": "^2.2.0", "jest-circus": "^25.5.4", "jest-cli": "^25.5.4", + "jest-environment-jsdom-thirteen": "^1.0.1", "jest-raw-loader": "^1.0.1", "jimp": "^0.9.6", "json5": "^1.0.1", diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png deleted file mode 100644 index bc41213edc7b60126bad788f5af94d995fa1a4cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11622 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 O8cW?999lx{14?i>yU_n~ok=k&Gcnr$v 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 diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png deleted file mode 100644 index 3716867865e4430d4080d00f0c0e16b2b67ebc91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12964 zcmeHuS3pxy)9pzJf}j{diXcV0A_S3MgVZ2RdItprq>1zr2#N|wkpPp$dc!|3Uxn-k0yb-v^&KCpmlXvuDq&S!*VQX=|#Sr(mQ20N^}IRZ#~3px`AG zAU^~C+433Q2LLtzr6{lGW3e*rACRh_*1oak%iZ|o(~}MjrJwhg%)c-VCcHKZ_m;mc zrF03UBOel8vjuhhQiUnBs^#syq-`H7+B!;WE>D?5AFr={DOd+?4lgTYCpVekbSG}B zr(mnxdwWt-JNJ4Bl|fFMJ???cJ8M==z7OUubFh%JVE`yO8$1BZl8+|fRU@(_!Ubh@GGGwq{%zpGWLOr0~dgLH|TK}!{VR5vo zWKrg;5fNqrPloxrI2A}jTuI3RT-5VVaE4T55KvYw7k`s5`+>nxh0&3!N_m!xnYU2G zKvJYK@b|mm!Ywkux{?jQ*-^`$ z)n_SIYgKdk!y;Ek`KZ}7V`~l#^+K0C@ z&jklSGEXXo+VMm;zWON!_ivvn+nZAW?K9$9N}0;wtBOd9cPXzkzZ%?_I~hcI|247j|g_Qo|O@OYerIg1QHVDRr(V6CF+@Z@?8YD!60S5 zP$UYfM26X@?tA6&Oe8tRL`q__HajYsw=?Xn(_4F${bP}|T6DjO@ z60!*>TS1(7Yb7p=aW+~x<0o|qa%C}qV2>zdg-tsIfMk+$Qpsl~z{>FyMs2EyYMrOq z3YJ33B#b5*Z`Qghsj@R3=UxV|7HMD)v9y5G-gpsSxDs$Q3OjBUy+gh ze)7b%FgvJYLBd#uqc9S?YG@Obr#Rg|a=6&+GkiV={Z=yOy zyB`GpxIm0a=;`M?kQvz)(BDkbI!9ELk|{lSPBJ`spqK70!%<+-IjTWxjKHHLb%Ww7 z)?8k^#wmzw_sAKb(R4^F21+>aeB+Tr@2%jz;R74b;0VS-?Qw%n9NUn$!+0|TB^iKv zP=MvvE&STvm&Xa{rM)KoM9T(*Ncl^8bCr4gxTlsvirvFJHO3rYmDb25FS4x4pXCJ1LD-%XyCl^neGETwku+`LZyrK0f)EI50YgE zeyeuAq+EEt|N=KV~z0RYmO?NuN!SEqiyhP*8;Pw-bXj{rnw9k46X4I&nC@=UjQ}+60eU- z7G`fIJ{E*zUgD2Z%H#x{2xWF90(lX-vJw*|y?X|gN9(pVi#0D&<)d9W;u8_u`GwZp z**++!u9g?wkBdm-ymr?ra#zD6QmyK^D%7NkS;quexN8$N7?*&jMIY->aRPB~Ti=t% zaUucUu$NCS^4_*Wwf_>D+w_gD@KaY_kz5Z;Idfui^vSS5tWHnojo7S=z~trw4JpE! zlAh&5AXIDbmWH#0LEG5dKFcn77)GZ;oGus(x;U4cn;?`FmXNI(JK+7B)mCT1sVVbT zUnpWP@nk`bnWxYeYmq*0Gp$yYt}}eUwiGxt9)yhd-;>R<>HhRzC-a5Xl?3HD;g7Hu z*xCGwh@+z1Dsxk}_SM~on7tSqi^R>TQO%KxsvOd9eY8N)OdXy(tJ+RN#dGyMxI1wH zDx_5zs4G8qZAinsXcyFvv)$^d}^{y?xA`3+-X`L>cy_jjdy<9>l$30?kCtuJv| z9VPLXeu=Nrs8tE{`A1D5AVqrR+lrn7`*k+`-F{KLk`}ssyKb%fpqEBqX|9$Po+{s9 zBo`Pk8;Fy$<65>sS@X-2B4|G?u7vA8Q6*7@HofEZtad>hjso8*Mw!Si1A%XC2%OVC zvn=l>se{Zm;*I-s$Nfp*07X;^37nw_fLN71mc9@i7~D#U&*$yhyjj2H!lbgUA|=SNnj|hYWHObh1_Fvh~R!fZWT+4guG$<3)|kHU*yt>yyK$v>$jDz z$OF94FaD~-`REjNj zopx*Li;BnY*(Yr7s1l3Ss#;w`Z>)iH^+(%ryix)2j)=^>61*D~Wd1U}=V(J*L#KS3 z=huxNhqbjNEedK?qigwgZcDV`Lf!&X?$wO5g`~PBZHZ`C0E*e%_+TXt9_OP68hImd zqq=q8oGkyxsrzT017`Fex@s9(FTP8=Wd{fb>6P2gmm*fxdtZ4djhYnYI+{4D7<*C! zT{E+tK9A|$on)JIHm#Jl?uEzBA*=^Z_ zJ19hx@cIm7BaN*6+IXGAS2P?rqtQ-tk@qx&IKT8??BMwtzmzc2X?)=(kt2D6c$7@i z!wjTrmv7V7Zx4A+50o#SbZ!iM7T_|CeJaWZoJ)@4Vb@m00NSdd862e*55czo)|`L!zwA1gV)i!|fA1+LJ-Ny66fN7KiLzfz7$ZSj5` z2K`phmYG(Be#IUHMP?`QYwZ-IMovL$lIMRx%>Z1i=u$iAc`a3@Yi$|T{lprD&e67H zMJ9Ei!;Ex693>=Xw>D0-iUVMn_gE&md6ag@WDei|~?w^2K1K%VfE?(M<^9K2{t%^y?=<|>Hp zSn!unOUbh#Jd;f@FUx%lMF#xt8>Z(%ix4E+svI7gzOy=iLu9=MTYD^UUjpgI#?I#L%-fqbDiW}`O-7ngI=Tv%Ogk(ag?q%;l=y+6YSI|CY+ zA%d}Ao^3O6^=-787ndPWPJTnP@lH%&@-_{%L0a;tWO2TRBETCS_7u(#0fvR(Lus;L zBhZaDgyu%C7y_RHF0>#cxC&f57j8+2-f_dnAc61ABCvPd;Nday^RN)10TgT}q4o5u zIXK>9(N84hS9blK3;dsI${jsx;JFH%J2;83f3yICse)a9SET^Fd3JhC?t$y)5`*%U zMv?LRwQG>E#p}rSOP7b@M?Ts&1$5%UXks!@bcGm-we(v5-qHPib41ul`u6|{&QeaAaBmEmk(+ z=n`>OtqNV8I&`7aosSuq;ZDeY&~0xq8u)vf3FN7qrWmEq;PHmsw5uyhKpM6`E1%;V z*K*ZStE$j<1f#c_5n?sj&DET+wSU8N9(+Ye=0oebFMsPFaU5Sq+#-s?l@}qA@D1BjkPz)eh;F=tR(5J zOx|4vs?1445!?8V)c0Bn^^*wEQc}kc`pz&}ctfYoAz7u`DUc0l(Y^SwUk#w?e0LQWz`ZSQ! zn%h>+;i9MbS>PMX)z})X!TcMZB}2mrVM1;1Zh7Wqhruvi&KLVakjQQ`FE;C)X)_Xdl|dKOllZ_f&e9={YZEYq~Z-FWR}ZnoEYE3tcCtqbjnF8m-| zylqPQ&5IVOt5?u1B_F0NB-^g9qptT<>V*}H`Nc=Is;eFm$6{()@maO81w z%j_*LD{gGOCmVRfsKHy-C9%T%JDoi{@lJcHcVE_00B$$U3ubL9B~{Gt zaBez{@-f0NZvVR2YhzT_4pZ!JltqW zw?n)dP{Jxp@$v^6|6m+h@oY*?sRpV2RZ4)8kh36$Ri-Nfwe+h{P)zOU>&2i z<94--(c=B>ux``NC9lr(QU~Xq1kbl`z+)P?Lx)ysvoI#j^_o6<5 zdLl$7sE+k|Fq7+)0!LM#;;3k+eZWUcAE|G9H8z1)tqmOs9XU!t4W(liFN6S_5~zo8 z=j)80{E2|}vakeIa4KkiQ8K73xd&qEjstUUg@m5+LFdDwh_n*k@Y>0G9W2>v|L^q1fq=W@WT$GdG@zXT-- zd~-6`;qisiWIQBWK?vW&!)$M(`Os{jM(EhMYIZhAYqBCt-Q!if4A*g&CC9C5Hzn)C z@xapt5mJK!6pW9ZE}0j#U@RB9lMRG5F7Gx>YBsHWRkWPBi1HRLw(puMM&LJ+GJ+m% zFErU5ti}vDitae!b`+`u7A#vwKe>SuVYI;AX5`BA)6(;9QKCn}3Rz;ezU9(tsL;{? zGu8%a6%T`p`cRzID#ABhLrGv4Krx;<2jaDem=J}45}PGpbTlyHzoUfSAncoK@FmY6 zvab1%ve5vVW}Yvyt=qZVRy})uTZ<;@TxdVl_)Q>;1|-2e^^~lTfKk!{s=}7;a&cV~ zu1O)X$Ixbr+i~T+249NkDm7psZ811EkkY$=8z9Tkkd$pVR-Sv;MjJV=mt(IP4%$*2 z%!A^{2Vrk$c-KpgHQHW}wylZc1}*Oe&(3~6zJ6`nV`lMXa-vv!2KV=1@cH3v&>Oki zl?PfWmUo}Ic;>P95`xm>r`xl8Zwe*56q}D%lslLlzHgFlXLT;BRWz^@D|V{O9ov?m z0!t6;`WWCl!6v_u9r_Fh^EqgDIf)P%ke zXCpPAoE~`$c;unqjYlJqAAv_RD=Nn0{+#B66>lB9nr4FnW}@*l*Gv!vSq@4=9%9U8 z6kx|jn;8N{dYVZT@<5+KTi($a9@S@eM~K*omN+$3h0eN0HFtgMXlQg(nk|)fh!uh( zf&VDz5EJSSPdhosWfnDp{pD6MvO2LCRblY(e44BPafcB{8!)B$Ga3pvy_kf28+6Gu zfWaBSj-r*^Imck>wWT1!U3f}k{I4<1EJBzo-^-CQTtC~BuN9}zBJ_Sww9u6fEpW^~} zGHwQaKIiE1EarHJ>BEv`BjNqZN+ID`cE4peGMT4yi62W$6xJEs#G>_t2<$`O+**6S z;Vs{Di(iEPCnvLiznzJNd|&Psy%MK7$*MY6Jn4wPzNl4RE+nZvIJR<N79HqoU43-Y@TK^>M@&0@*c?by1 zhPRiSoqZqLH0@BInqP*y{9r4& z&eGEJTOxd!@b|N}N@r!+t??eMJ#JEs zH*Ss{`>;UC8&17Oj7?Axs(%*P$mlU(U6pmg@jrFwP6WQ<0#7>Zl(|Gr7U8I9#)568 zTbA`EZDSv|THWDDD{rtCP!A2hN}X>7)EIUJD7qYcmKOc}jNZa~Xv6ZruAq328kni; zPukJW3R@0BO<4Q0Mw>VW*{D~PAj%0PO_|4?ItQ~pF!*`xl8`RG_A*CwQ5Ohx%y1iTa}%8w(-LB38M8dR#pfLNUQV#h<3>&9@f#igUp}zg z^xr9v3sRWl7wVZh5Vo`h8P27`gMi0H>~#P@ZF%}zfWw2w$Xksu?_CMWf+y8E!V9Lr zLg|tFCM&K#f9I%8vAuI*LrBsIY3X3yE8_2p{@rj}jOBXk6YGl%tDp+}A$xCs(;)LP zq-D4;Z}lvVGx}>s!+qpuZ~hd)HHM_!=wCA|O=mFXg>r<#9acehb1~Ftpz>gS_0d!S z`eV_zbW5|mE6;#aHUU5tIJFA^t4qw49y5l4(+ysIsJ5lK;1#2+TlG9W^;KhZ-RFV% zKK%N<%dMr8j*8O>GR?$Rv3WG>r-(bEJ?{t2P!|M1C#t(oxZ*Qao9I-dJZOyd^DCBF zPadB=7Ja?Beg$@$228%9Zu{HHrxtM@YQG`3ip^+`6e&&GB;i0?A!GX$WvZGA9gcJF zGa|%*p4TVe>x*y7z5J$TbTH9v7u`M8!}D!+>W-KhqHxHz!tZ0x{tlh)Y)N}S;Hm;y z9*yYc-bqDeySa$}RPx8v7xQfJWB|fe5jIpR292ix43fuW+;9^$@2?fwHBBgok#R=) zzP~$E>ohav&?;=<0*If;1$|J_TFc)>tlu5AZ;QW0p#Nzi*7VZSue%wP0Y}B^2k{>) z4u0*9RXy{LX4u?HH|$zqHmKX}O4?51qmWSe(r`dD6rSnd=#Q*y53nZIs58ufX=3jb z$KbH%s^G?3vNb*rKCksARu43I5pP)c9tJxrjcP7Qx*Tg!RGPQ|q5VOthv)Cu#KmpR zH>tXIZ@V?RBYYRGcc$|@+_5ZeGe_S&V%;Z5?O@5)usu?dTPb%ReEHJ-fQ9H(GSQ#Z z`~x330M_&t#s{nMVw0990dHjYB%tK$=BL%}=jxdzkj$0Us*C-{?#-auA+gj2W5DkV z6D7JpiwoM~>(1(HYx#DE1$EGh$JrG(etoyvT$+sjVmUFf#v0(XCiO?0GR9pBwP zQ606Yvm(|QD~M{-DQC8wa(8Qztf?Oy+Qk5zQlr*=)W`AdZsj`jNu;S`(;#|k>ZF6m zC=7sS$hq@b#y0sTMiW}`dNs2aq*y=O9@G}VLaec(h z=5oqjPV>sWND;%e#_f*FD=XYG0bb;3Y})3hnGh`@1(hHL<=KpqR;_#Ad7=Gb474hj zW=^9!IxS^0R|52yjbsoyvQ$=lli$xNUOsq$I#8Xsk%jM`7&%+$NZE*5|FFqAC3e;Q zd6+Ot)Ln@~R^_(AN!cJr__E)SUDF3EO*Cpp%E}ZZDF{dz8Js~V_#qjlpm5UiL00;C zuSv`9Nfu>qffT{gtA<1I=KzH>-Jd}4i~}{&pX$eTg!T%kDW)185c49{11_UmJ_kM? z|FS3cfDj63?@72T!vesBxhr{jiIE5ufMSYzt~nyiIL0?fkSAD{n{Na6Zmwvpg{uP$ z#L1{L7ssxG@OtYMUNxbZ>;eM94_${iBJ9oukeD>OjDPXvhheTV%67AZ)CSV>^s4sl z*U%DZOI0FNpkcVfLU7#^<6%ORAl73lr=7tE+*?(uqte9y@VTkQ2@*&m(tHt+9%G2ZU^pO942OM!yj6Agilbe1NAhq>Q1swj zNXhfs>bW6F+uqdOrX8{!)(?aYhs+WZ1m77F{9r^)D4#dq7@JH2iV*Alp?|G z&;I>41v03+*Kw{$aIpOh+)1;$A#m?YZPMg=dimTZH*93{9jkWh)9iZN8@?412lARs z7}l2EVxT^@qNGhVJ{e_z+=^uI{yK=y->U&Y zflE*i^8M#(EX}~N3XXq~A3=14#Q(6;71H0&K%x66;>vs22<#DKgZ&htZ* zp5%UU_TBMtZs5`tE;2UlSkOetSD2J8kU=a#i(YR@qqlI1Xbc4euXxePuxW!8?o4r> zsN0z{P+3xpR_`GA42e*fI1Qw9$T7E{nAOrC>$ zv~vnnMN@ts82>I|0ZNSq1`?&ezo+|d0Mhr-l##zntltLjS{Znuf7h=D6KgfD==t9z z=)pTg8}j1c^=rVWt=WDc@^^^_P;>a)JQM!6<=9dTZmL+U@amuL<-aZdJH>yF`Y#qw zQRE-o{6o}#N#kE$Jf$1|*v&rz_y6RmhjQLtVU?sWHf6#8J^@flnu;a2t)Bc3^6e=| diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png deleted file mode 100644 index 6ea090562d46e774fcbd12682cccd36f39bb791f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10359 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 Z>~6C!wyQT20TvVjmn`iO73a|p{u5H(=8pgX diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js deleted file mode 100644 index 4a6e9e7765213e..00000000000000 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js +++ /dev/null @@ -1,202 +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 { ImageComparator } from 'test_utils/image_comparator'; -import basicdrawPng from './basicdraw.png'; -import afterresizePng from './afterresize.png'; -import afterparamChange from './afterparamchange.png'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExprVis } from '../../../../../../plugins/visualizations/public/expressions/vis'; - -// Replace with mock when converting to jest tests -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; -// Will be replaced with new path when tests are moved -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createTagCloudVisTypeDefinition } from '../../../../../../plugins/vis_type_tagcloud/public/tag_cloud_type'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createTagCloudVisualization } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud_visualization'; -import { npStart } from 'ui/new_platform'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setFormatService } from '../../../../../../plugins/vis_type_tagcloud/public/services'; - -const THRESHOLD = 0.65; -const PIXEL_DIFF = 64; -describe('TagCloudVisualizationTest', function () { - let domNode; - let vis; - let imageComparator; - - const dummyTableGroup = { - columns: [ - { - id: 'col-0', - title: 'geo.dest: Descending', - }, - { - id: 'col-1', - title: 'Count', - }, - ], - rows: [ - { 'col-0': 'CN', 'col-1': 26 }, - { 'col-0': 'IN', 'col-1': 17 }, - { 'col-0': 'US', 'col-1': 6 }, - { 'col-0': 'DE', 'col-1': 4 }, - { 'col-0': 'BR', 'col-1': 3 }, - ], - }; - const TagCloudVisualization = createTagCloudVisualization({ - colors: { - seedColors, - }, - }); - - before(() => setFormatService(npStart.plugins.data.fieldFormats)); - - beforeEach(ngMock.module('kibana')); - - describe('TagCloudVisualization - basics', function () { - beforeEach(async function () { - const visType = new BaseVisType(createTagCloudVisTypeDefinition({ colors: seedColors })); - setupDOM('512px', '512px'); - imageComparator = new ImageComparator(); - vis = new ExprVis({ - type: visType, - params: { - bucket: { accessor: 0, format: {} }, - metric: { accessor: 0, format: {} }, - }, - data: {}, - }); - }); - - afterEach(function () { - teardownDOM(); - imageComparator.destroy(); - }); - - it('simple draw', async function () { - const tagcloudVisualization = new TagCloudVisualization(domNode, vis); - - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: false, - params: true, - aggs: true, - data: true, - uiState: false, - }); - - const svgNode = domNode.querySelector('svg'); - const mismatchedPixels = await imageComparator.compareDOMContents( - svgNode.outerHTML, - 512, - 512, - basicdrawPng, - THRESHOLD - ); - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); - - it('with resize', async function () { - const tagcloudVisualization = new TagCloudVisualization(domNode, vis); - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: false, - params: true, - aggs: true, - data: true, - uiState: false, - }); - - domNode.style.width = '256px'; - domNode.style.height = '368px'; - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: true, - params: false, - aggs: false, - data: false, - uiState: false, - }); - - const svgNode = domNode.querySelector('svg'); - const mismatchedPixels = await imageComparator.compareDOMContents( - svgNode.outerHTML, - 256, - 368, - afterresizePng, - THRESHOLD - ); - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); - - it('with param change', async function () { - const tagcloudVisualization = new TagCloudVisualization(domNode, vis); - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: false, - params: true, - aggs: true, - data: true, - uiState: false, - }); - - domNode.style.width = '256px'; - domNode.style.height = '368px'; - vis.params.orientation = 'right angled'; - vis.params.minFontSize = 70; - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: true, - params: true, - aggs: false, - data: false, - uiState: false, - }); - - const svgNode = domNode.querySelector('svg'); - const mismatchedPixels = await imageComparator.compareDOMContents( - svgNode.outerHTML, - 256, - 368, - afterparamChange, - THRESHOLD - ); - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); - }); - - function setupDOM(width, height) { - domNode = document.createElement('div'); - domNode.style.top = '0'; - domNode.style.left = '0'; - domNode.style.width = width; - domNode.style.height = height; - domNode.style.position = 'fixed'; - domNode.style.border = '1px solid blue'; - domNode.style['pointer-events'] = 'none'; - document.body.appendChild(domNode); - } - - function teardownDOM() { - domNode.innerHTML = ''; - document.body.removeChild(domNode); - } -}); diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap new file mode 100644 index 00000000000000..e32425a0954291 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foobarfoobar"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap new file mode 100644 index 00000000000000..dbc3dd1202cbd2 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CNINUSDEBR"`; + +exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CNINUSDEBR"`; + +exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CNINUSDEBR"`; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js similarity index 72% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js index 35c7b77687b94f..89a6a67bcb2fb4 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js @@ -17,22 +17,27 @@ * under the License. */ -import expect from '@kbn/expect'; import _ from 'lodash'; import d3 from 'd3'; +import 'jest-canvas-mock'; import { fromNode, delay } from 'bluebird'; -import { ImageComparator } from 'test_utils/image_comparator'; -import simpleloadPng from './simpleload.png'; +import { TagCloud } from './tag_cloud'; +import { setHTMLElementOffset, setSVGElementGetBBox } from '../../../../test_utils/public'; -// Replace with mock when converting to jest tests -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; -// Will be replaced with new path when tests are moved -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TagCloud } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud'; +describe('tag cloud tests', () => { + let SVGElementGetBBoxSpyInstance; + let HTMLElementOffsetMockInstance; + + beforeEach(() => { + setupDOM(); + }); + + afterEach(() => { + SVGElementGetBBoxSpyInstance.mockRestore(); + HTMLElementOffsetMockInstance.mockRestore(); + }); -describe('tag cloud tests', function () { const minValue = 1; const maxValue = 9; const midValue = (minValue + maxValue) / 2; @@ -100,16 +105,15 @@ describe('tag cloud tests', function () { let domNode; let tagCloud; - const colorScale = d3.scale.ordinal().range(seedColors); + const colorScale = d3.scale + .ordinal() + .range(['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']); function setupDOM() { domNode = document.createElement('div'); - domNode.style.top = '0'; - domNode.style.left = '0'; - domNode.style.width = '512px'; - domNode.style.height = '512px'; - domNode.style.position = 'fixed'; - domNode.style['pointer-events'] = 'none'; + SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(); + HTMLElementOffsetMockInstance = setHTMLElementOffset(512, 512); + document.body.appendChild(domNode); } @@ -126,42 +130,39 @@ describe('tag cloud tests', function () { sqrtScaleTest, biggerFontTest, trimDataTest, - ].forEach(function (test) { + ].forEach(function (currentTest) { describe(`should position elements correctly for options: ${JSON.stringify( - test.options - )}`, function () { - beforeEach(async function () { - setupDOM(); + currentTest.options + )}`, () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(test.data); - tagCloud.setOptions(test.options); + tagCloud.setData(currentTest.data); + tagCloud.setOptions(currentTest.options); await fromNode((cb) => tagCloud.once('renderComplete', cb)); }); afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(test.expected, textElements, tagCloud); + verifyTagProperties(currentTest.expected, textElements, tagCloud); }) ); }); }); - [5, 100, 200, 300, 500].forEach(function (timeout) { - describe(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, function () { - beforeEach(async function () { - setupDOM(); - + [5, 100, 200, 300, 500].forEach((timeout) => { + describe(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => { + beforeEach(async () => { //TagCloud takes at least 600ms to complete (due to d3 animation) //renderComplete should only notify at the last one tagCloud = new TagCloud(domNode, colorScale); @@ -176,16 +177,16 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(logScaleTest.expected, textElements, tagCloud); }) @@ -193,9 +194,8 @@ describe('tag cloud tests', function () { }); }); - describe('should use the latest state before notifying (when modifying options multiple times)', function () { - beforeEach(async function () { - setupDOM(); + describe('should use the latest state before notifying (when modifying options multiple times)', () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); @@ -205,53 +205,53 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(logScaleTest.expected, textElements, tagCloud); }) ); }); - describe('should use the latest state before notifying (when modifying data multiple times)', function () { - beforeEach(async function () { - setupDOM(); + describe('should use the latest state before notifying (when modifying data multiple times)', () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); tagCloud.setData(trimDataTest.data); + await fromNode((cb) => tagCloud.once('renderComplete', cb)); }); afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(trimDataTest.expected, textElements, tagCloud); }) ); }); - describe('should not get multiple render-events', function () { + describe('should not get multiple render-events', () => { let counter; - beforeEach(function () { + beforeEach(() => { counter = 0; - setupDOM(); + return new Promise((resolve, reject) => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); @@ -281,31 +281,32 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(logScaleTest.expected, textElements, tagCloud); }) ); }); - describe('should show correct data when state-updates are interleaved with resize event', function () { - beforeEach(async function () { - setupDOM(); + describe('should show correct data when state-updates are interleaved with resize event', () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(logScaleTest.data); tagCloud.setOptions(logScaleTest.options); await delay(1000); //let layout run - domNode.style.width = '600px'; - domNode.style.height = '600px'; + + SVGElementGetBBoxSpyInstance.mockRestore(); + SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(600, 600); + tagCloud.resize(); //triggers new layout setTimeout(() => { //change the options at the very end too @@ -317,26 +318,23 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(baseTest.expected, textElements, tagCloud); }) ); }); - describe(`should not put elements in view when container is too small`, function () { - beforeEach(async function () { - setupDOM(); - domNode.style.width = '1px'; - domNode.style.height = '1px'; + describe(`should not put elements in view when container is too small`, () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); @@ -345,10 +343,10 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it('completeness should not be ok', function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE); + test('completeness should not be ok', () => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); }); - it('positions should not be ok', function () { + test('positions should not be ok', () => { const textElements = domNode.querySelectorAll('text'); for (let i = 0; i < textElements; i++) { const bbox = textElements[i].getBoundingClientRect(); @@ -357,96 +355,73 @@ describe('tag cloud tests', function () { }); }); - describe(`tags should fit after making container bigger`, function () { - beforeEach(async function () { - setupDOM(); - domNode.style.width = '1px'; - domNode.style.height = '1px'; - + describe(`tags should fit after making container bigger`, () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode((cb) => tagCloud.once('renderComplete', cb)); //make bigger - domNode.style.width = '512px'; - domNode.style.height = '512px'; + tagCloud._size = [600, 600]; tagCloud.resize(); await fromNode((cb) => tagCloud.once('renderComplete', cb)); }); afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); }); - describe(`tags should no longer fit after making container smaller`, function () { - beforeEach(async function () { - setupDOM(); + describe(`tags should no longer fit after making container smaller`, () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode((cb) => tagCloud.once('renderComplete', cb)); //make smaller - domNode.style.width = '1px'; - domNode.style.height = '1px'; + tagCloud._size = []; tagCloud.resize(); await fromNode((cb) => tagCloud.once('renderComplete', cb)); }); afterEach(teardownDOM); - it('completeness should not be ok', function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE); + test('completeness should not be ok', () => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); }); }); - describe('tagcloudscreenshot', function () { - let imageComparator; - beforeEach(async function () { - setupDOM(); - imageComparator = new ImageComparator(); - }); - - afterEach(() => { - imageComparator.destroy(); - teardownDOM(); - }); + describe('tagcloudscreenshot', () => { + afterEach(teardownDOM); - it('should render simple image', async function () { + test('should render simple image', async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode((cb) => tagCloud.once('renderComplete', cb)); - const mismatchedPixels = await imageComparator.compareDOMContents( - domNode.innerHTML, - 512, - 512, - simpleloadPng, - 0.5 - ); - expect(mismatchedPixels).to.be.lessThan(64); + expect(domNode.innerHTML).toMatchSnapshot(); }); }); function verifyTagProperties(expectedValues, actualElements, tagCloud) { - expect(actualElements.length).to.equal(expectedValues.length); + expect(actualElements.length).toEqual(expectedValues.length); expectedValues.forEach((test, index) => { try { - expect(actualElements[index].style.fontSize).to.equal(test.fontSize); + expect(actualElements[index].style.fontSize).toEqual(test.fontSize); } catch (e) { throw new Error('fontsize is not correct: ' + e.message); } try { - expect(actualElements[index].innerHTML).to.equal(test.text); + expect(actualElements[index].innerHTML).toEqual(test.text); } catch (e) { throw new Error('fontsize is not correct: ' + e.message); } @@ -470,14 +445,14 @@ describe('tag cloud tests', function () { debugInfo: ${JSON.stringify(tagCloud.getDebugInfo())}`; try { - expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).to.be(shouldBeInside); + expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).toBe(shouldBeInside); } catch (e) { throw new Error( 'top boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message ); } try { - expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).to.be(shouldBeInside); + expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).toBe(shouldBeInside); } catch (e) { throw new Error( 'bottom boundary of tag should have been ' + @@ -486,14 +461,14 @@ describe('tag cloud tests', function () { ); } try { - expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).to.be(shouldBeInside); + expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).toBe(shouldBeInside); } catch (e) { throw new Error( 'left boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message ); } try { - expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).to.be(shouldBeInside); + expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).toBe(shouldBeInside); } catch (e) { throw new Error( 'right boundary of tag should have been ' + @@ -532,7 +507,7 @@ describe('tag cloud tests', function () { } function handleExpectedBlip(assertion) { - return function () { + return () => { if (!shouldAssert()) { return; } diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js new file mode 100644 index 00000000000000..7f96066c16076f --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js @@ -0,0 +1,176 @@ +/* + * 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 'jest-canvas-mock'; + +import { createTagCloudVisTypeDefinition } from '../tag_cloud_type'; +import { createTagCloudVisualization } from './tag_cloud_visualization'; +import { setFormatService } from '../services'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { setHTMLElementOffset, setSVGElementGetBBox } from '../../../../test_utils/public'; + +const seedColors = ['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']; + +describe('TagCloudVisualizationTest', () => { + let domNode; + let vis; + let SVGElementGetBBoxSpyInstance; + let HTMLElementOffsetMockInstance; + + const dummyTableGroup = { + columns: [ + { + id: 'col-0', + title: 'geo.dest: Descending', + }, + { + id: 'col-1', + title: 'Count', + }, + ], + rows: [ + { 'col-0': 'CN', 'col-1': 26 }, + { 'col-0': 'IN', 'col-1': 17 }, + { 'col-0': 'US', 'col-1': 6 }, + { 'col-0': 'DE', 'col-1': 4 }, + { 'col-0': 'BR', 'col-1': 3 }, + ], + }; + const TagCloudVisualization = createTagCloudVisualization({ + colors: { + seedColors, + }, + }); + + const originTransformSVGElement = window.SVGElement.prototype.transform; + + beforeAll(() => { + setFormatService(dataPluginMock.createStartContract().fieldFormats); + Object.defineProperties(window.SVGElement.prototype, { + transform: { + get: () => ({ + baseVal: { + consolidate: () => {}, + }, + }), + configurable: true, + }, + }); + }); + + afterAll(() => { + SVGElementGetBBoxSpyInstance.mockRestore(); + HTMLElementOffsetMockInstance.mockRestore(); + window.SVGElement.prototype.transform = originTransformSVGElement; + }); + + describe('TagCloudVisualization - basics', () => { + beforeEach(async () => { + const visType = createTagCloudVisTypeDefinition({ colors: seedColors }); + setupDOM(512, 512); + + vis = { + type: visType, + params: { + bucket: { accessor: 0, format: {} }, + metric: { accessor: 0, format: {} }, + scale: 'linear', + orientation: 'single', + }, + data: {}, + }; + }); + + test('simple draw', async () => { + const tagcloudVisualization = new TagCloudVisualization(domNode, vis); + + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: false, + params: true, + aggs: true, + data: true, + uiState: false, + }); + + const svgNode = domNode.querySelector('svg'); + expect(svgNode.outerHTML).toMatchSnapshot(); + }); + + test('with resize', async () => { + const tagcloudVisualization = new TagCloudVisualization(domNode, vis); + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: false, + params: true, + aggs: true, + data: true, + uiState: false, + }); + + domNode.style.width = '256px'; + domNode.style.height = '368px'; + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: true, + params: false, + aggs: false, + data: false, + uiState: false, + }); + + const svgNode = domNode.querySelector('svg'); + expect(svgNode.outerHTML).toMatchSnapshot(); + }); + + test('with param change', async function () { + const tagcloudVisualization = new TagCloudVisualization(domNode, vis); + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: false, + params: true, + aggs: true, + data: true, + uiState: false, + }); + + SVGElementGetBBoxSpyInstance.mockRestore(); + SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(256, 368); + + HTMLElementOffsetMockInstance.mockRestore(); + HTMLElementOffsetMockInstance = setHTMLElementOffset(256, 386); + + vis.params.orientation = 'right angled'; + vis.params.minFontSize = 70; + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: true, + params: true, + aggs: false, + data: false, + uiState: false, + }); + + const svgNode = domNode.querySelector('svg'); + expect(svgNode.outerHTML).toMatchSnapshot(); + }); + }); + + function setupDOM(width, height) { + domNode = document.createElement('div'); + + HTMLElementOffsetMockInstance = setHTMLElementOffset(width, height); + SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(width, height); + } +}); diff --git a/src/test_utils/public/helpers/index.ts b/src/test_utils/public/helpers/index.ts index 79dc29e83bc3b7..c8447743ee287e 100644 --- a/src/test_utils/public/helpers/index.ts +++ b/src/test_utils/public/helpers/index.ts @@ -24,3 +24,5 @@ export { WithStore } from './redux_helpers'; export { WithMemoryRouter, WithRoute, reactRouterMock } from './router_helpers'; export * from './utils'; + +export { setSVGElementGetBBox, setHTMLElementOffset } from './jsdom_svg_mocks'; diff --git a/src/test_utils/public/helpers/jsdom_svg_mocks.ts b/src/test_utils/public/helpers/jsdom_svg_mocks.ts new file mode 100644 index 00000000000000..dbc8266f663f19 --- /dev/null +++ b/src/test_utils/public/helpers/jsdom_svg_mocks.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. + */ + +export const setSVGElementGetBBox = ( + width: number, + height: number, + x: number = 0, + y: number = 0 +) => { + const SVGElementPrototype = SVGElement.prototype as any; + const originalGetBBox = SVGElementPrototype.getBBox; + + // getBBox is not in the SVGElement.prototype object by default, so we cannot use jest.spyOn for that case + SVGElementPrototype.getBBox = jest.fn(() => ({ + x, + y, + width, + height, + })); + + return { + mockRestore: () => { + SVGElementPrototype.getBBox = originalGetBBox; + }, + }; +}; + +export const setHTMLElementOffset = (width: number, height: number) => { + const offsetWidthSpy = jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get'); + offsetWidthSpy.mockReturnValue(width); + + const offsetHeightSpy = jest.spyOn(window.HTMLElement.prototype, 'offsetHeight', 'get'); + offsetHeightSpy.mockReturnValue(height); + + return { + mockRestore: () => { + offsetWidthSpy.mockRestore(); + offsetHeightSpy.mockRestore(); + }, + }; +}; diff --git a/src/test_utils/public/index.ts b/src/test_utils/public/index.ts new file mode 100644 index 00000000000000..4f46dfe1578db2 --- /dev/null +++ b/src/test_utils/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 { setSVGElementGetBBox, setHTMLElementOffset } from './helpers'; diff --git a/yarn.lock b/yarn.lock index 7e44780389531c..eb1943c5cd00cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10379,6 +10379,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-convert@~0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0= + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -11444,6 +11449,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssfontparser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" + integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M= + csso@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.2.tgz#e5f81ab3a56b8eefb7f0092ce7279329f454de3d" @@ -18922,6 +18932,14 @@ iterall@^1.2.2: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== +jest-canvas-mock@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.2.0.tgz#45fbc58589c6ce9df50dc90bd8adce747cbdada7" + integrity sha512-DcJdchb7eWFZkt6pvyceWWnu3lsp5QWbUeXiKgEMhwB3sMm5qHM1GQhDajvJgBeiYpgKcojbzZ53d/nz6tXvJw== + dependencies: + cssfontparser "^1.2.1" + parse-color "^1.0.0" + jest-changed-files@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.5.0.tgz#141cc23567ceb3f534526f8614ba39421383634c" @@ -23755,6 +23773,13 @@ parse-bmfont-xml@^1.1.4: xml-parse-from-string "^1.0.0" xml2js "^0.4.5" +parse-color@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619" + integrity sha1-e3SLlag/A/FqlPU15S1/PZRlhhk= + dependencies: + color-convert "~0.5.0" + parse-entities@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.1.1.tgz#8112d88471319f27abae4d64964b122fe4e1b890" From 44925311fc8fe38bbed5269cbea6388d0263437e Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 6 Jul 2020 11:13:17 -0700 Subject: [PATCH 02/57] Fix kbn/optimizer tests (#70827) Co-authored-by: spalger --- .../basic_optimization.test.ts.snap | 66 +++++++++++++++++++ .../basic_optimization.test.ts | 14 ++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 1466865df8d989..211cfac3806ad7 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -1,5 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConfig 1`] = ` +OptimizerConfig { + "bundles": Array [ + Bundle { + "cache": BundleCache { + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, + "id": "bar", + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "type": "plugin", + }, + Bundle { + "cache": BundleCache { + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, + "id": "foo", + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "type": "plugin", + }, + ], + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 1, + "plugins": Array [ + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, + "extraPublicDirs": Array [], + "id": "bar", + "isUiPlugin": true, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, + "extraPublicDirs": Array [], + "id": "foo", + "isUiPlugin": true, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz, + "extraPublicDirs": Array [], + "id": "baz", + "isUiPlugin": false, + }, + ], + "profileWebpack": false, + "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "themeTags": Array [ + "v7dark", + "v7light", + ], + "watch": false, +} +`; + exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { await del(TMP_DIR); }); -// FLAKY: https://github.com/elastic/kibana/issues/70762 -it.skip('builds expected bundles, saves bundle counts to metadata', async () => { +it('builds expected bundles, saves bundle counts to metadata', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], @@ -75,7 +74,11 @@ it.skip('builds expected bundles, saves bundle counts to metadata', async () => expect(config).toMatchSnapshot('OptimizerConfig'); const msgs = await runOptimizer(config) - .pipe(logOptimizerState(log, config), toArray()) + .pipe( + logOptimizerState(log, config), + filter((x) => x.event?.type !== 'worker stdio'), + toArray() + ) .toPromise(); const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { @@ -168,8 +171,7 @@ it.skip('builds expected bundles, saves bundle counts to metadata', async () => `); }); -// FLAKY: https://github.com/elastic/kibana/issues/70764 -it.skip('uses cache on second run and exist cleanly', async () => { +it('uses cache on second run and exist cleanly', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], From da602fc783b5a8d444eb5651314802862502662c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 6 Jul 2020 14:25:56 -0400 Subject: [PATCH 03/57] fix nav link to be hidden and update access tag (#70607) --- .../security_solution/common/constants.ts | 10 +++++++++ .../security_solution/public/app/types.ts | 10 +-------- .../timeline/routes/create_timelines_route.ts | 2 +- .../timeline/routes/update_timelines_route.ts | 2 +- .../security_solution/server/plugin.ts | 21 ++++++++++++++----- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f547bc8185d02e..d32d9f01d61aec 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -35,6 +35,16 @@ export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; export const ENDPOINT_METADATA_INDEX = 'metrics-endpoint.metadata-*'; +export enum SecurityPageName { + alerts = 'alerts', + overview = 'overview', + hosts = 'hosts', + network = 'network', + timelines = 'timelines', + case = 'case', + management = 'management', +} + export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`; export const APP_ALERTS_PATH = `${APP_PATH}/alerts`; export const APP_HOSTS_PATH = `${APP_PATH}/hosts`; diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 4bd888e87bbdc7..4590f05e126312 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -18,16 +18,8 @@ import { State, SubPluginsInitReducer } from '../common/store'; import { Immutable } from '../../common/endpoint/types'; import { AppAction } from '../common/store/actions'; import { TimelineState } from '../timelines/store/timeline/types'; +export { SecurityPageName } from '../../common/constants'; -export enum SecurityPageName { - alerts = 'alerts', - overview = 'overview', - hosts = 'hosts', - network = 'network', - timelines = 'timelines', - case = 'case', - management = 'management', -} export interface SecuritySubPluginStore { initialState: Record; reducer: Record>; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts index 60ddaea367aedd..5bc4bec45dfb22 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts @@ -33,7 +33,7 @@ export const createTimelinesRoute = ( body: buildRouteValidation(createTimelineSchema), }, options: { - tags: ['access:siem'], + tags: ['access:securitySolution'], }, }, async (context, request, response) => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts index f59df151b69550..a622ee9b157062 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts @@ -31,7 +31,7 @@ export const updateTimelinesRoute = ( body: buildRouteValidation(updateTimelineSchema), }, options: { - tags: ['access:siem'], + tags: ['access:securitySolution'], }, }, // eslint-disable-next-line complexity diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a97f1eee56342c..356b6fca7e8ce1 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -39,7 +39,7 @@ import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; -import { APP_ID, APP_ICON, SERVER_APP_ID } from '../common/constants'; +import { APP_ID, APP_ICON, SERVER_APP_ID, SecurityPageName } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerResolverRoutes } from './endpoint/routes/resolver'; import { registerPolicyRoutes } from './endpoint/routes/policy'; @@ -70,6 +70,17 @@ export interface PluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} +const securitySubPlugins = [ + APP_ID, + `${APP_ID}:${SecurityPageName.overview}`, + `${APP_ID}:${SecurityPageName.alerts}`, + `${APP_ID}:${SecurityPageName.hosts}`, + `${APP_ID}:${SecurityPageName.network}`, + `${APP_ID}:${SecurityPageName.timelines}`, + `${APP_ID}:${SecurityPageName.case}`, + `${APP_ID}:${SecurityPageName.management}`, +]; + export class Plugin implements IPlugin { private readonly logger: Logger; private readonly config$: Observable; @@ -144,12 +155,12 @@ export class Plugin implements IPlugin Date: Mon, 6 Jul 2020 15:09:35 -0400 Subject: [PATCH 04/57] [EPM][Security Solution] Implementing dataset component templates (#70517) * Implementing dataset component templates * Fixing test * Temporary fix to include timestamp with any component template created * Update package registry docker image for CI. * Adapt to new registry filesystem layout. * Adjust tests to changed registry behavior. * Adding a test for mappings and settings overrides * Wrap all the tests in the docker check Co-authored-by: Elastic Machine Co-authored-by: Sonja Krause-Harder --- .../ingest_manager/common/types/models/epm.ts | 7 ++ .../__snapshots__/template.test.ts.snap | 3 + .../epm/elasticsearch/template/install.ts | 109 +++++++++++++++++- .../elasticsearch/template/template.test.ts | 30 +++++ .../epm/elasticsearch/template/template.ts | 8 +- .../ingest_manager/server/types/index.tsx | 1 + .../0.1.0/dataset/test/fields/fields.yml | 16 +++ .../overrides/0.1.0/dataset/test/manifest.yml | 9 ++ .../overrides/0.1.0/docs/README.md | 3 + .../0.1.0/img/logo_overrides_64_color.svg | 7 ++ .../overrides/0.1.0/manifest.yml | 20 ++++ .../apis/index.js | 1 + .../apis/install.ts | 85 ++++++++++++++ .../apis/list.ts | 2 +- .../apis/template.ts | 1 + 15 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/img/logo_overrides_64_color.svg create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/install.ts diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 3ee3039e9e1c49..0d2825f0aa80dd 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -175,6 +175,12 @@ export interface Dataset { package: string; path: string; ingest_pipeline: string; + elasticsearch?: RegistryElasticsearch; +} + +export interface RegistryElasticsearch { + 'index_template.settings'?: object; + 'index_template.mappings'?: object; } // EPR types this as `[]map[string]interface{}` @@ -272,6 +278,7 @@ export interface IndexTemplate { data_stream: { timestamp_field: string; }; + composed_of: string[]; _meta: object; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index f5fec020bf5b4d..848e65b7931ebc 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -94,6 +94,7 @@ exports[`tests loading base.yml: base.yml 1`] = ` "data_stream": { "timestamp_field": "@timestamp" }, + "composed_of": [], "_meta": { "package": { "name": "nginx" @@ -197,6 +198,7 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "data_stream": { "timestamp_field": "@timestamp" }, + "composed_of": [], "_meta": { "package": { "name": "coredns" @@ -1684,6 +1686,7 @@ exports[`tests loading system.yml: system.yml 1`] = ` "data_stream": { "timestamp_field": "@timestamp" }, + "composed_of": [], "_meta": { "package": { "name": "system" diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index a318aecf347d62..e14645bbbf5fb0 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -5,7 +5,13 @@ */ import Boom from 'boom'; -import { Dataset, RegistryPackage, ElasticsearchAssetType, TemplateRef } from '../../../../types'; +import { + Dataset, + RegistryPackage, + ElasticsearchAssetType, + TemplateRef, + RegistryElasticsearch, +} from '../../../../types'; import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; @@ -157,6 +163,98 @@ export async function installTemplateForDataset({ }); } +function putComponentTemplate( + body: object | undefined, + name: string, + callCluster: CallESAsCurrentUser +): { clusterPromise: Promise; name: string } | undefined { + if (body) { + const callClusterParams: { + method: string; + path: string; + ignore: number[]; + body: any; + } = { + method: 'PUT', + path: `/_component_template/${name}`, + ignore: [404], + body, + }; + + return { clusterPromise: callCluster('transport.request', callClusterParams), name }; + } +} + +function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch | undefined) { + let mappingsTemplate; + let settingsTemplate; + + if (registryElasticsearch && registryElasticsearch['index_template.mappings']) { + mappingsTemplate = { + template: { + mappings: { + ...registryElasticsearch['index_template.mappings'], + // temporary change until https://github.com/elastic/elasticsearch/issues/58956 is resolved + // hopefully we'll be able to remove the entire properties section once that issue is resolved + properties: { + // if the timestamp_field changes here: https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts#L309 + // we'll need to update this as well + '@timestamp': { + type: 'date', + }, + }, + }, + }, + }; + } + + if (registryElasticsearch && registryElasticsearch['index_template.settings']) { + settingsTemplate = { + template: { + settings: registryElasticsearch['index_template.settings'], + }, + }; + } + return { settingsTemplate, mappingsTemplate }; +} + +async function installDatasetComponentTemplates( + templateName: string, + registryElasticsearch: RegistryElasticsearch | undefined, + callCluster: CallESAsCurrentUser +) { + const templates: string[] = []; + const componentPromises: Array> = []; + + const compTemplates = buildComponentTemplates(registryElasticsearch); + + const mappings = putComponentTemplate( + compTemplates.mappingsTemplate, + `${templateName}-mappings`, + callCluster + ); + + const settings = putComponentTemplate( + compTemplates.settingsTemplate, + `${templateName}-settings`, + callCluster + ); + + if (mappings) { + templates.push(mappings.name); + componentPromises.push(mappings.clusterPromise); + } + + if (settings) { + templates.push(settings.name); + componentPromises.push(settings.clusterPromise); + } + + // TODO: Check return values for errors + await Promise.all(componentPromises); + return templates; +} + export async function installTemplate({ callCluster, fields, @@ -180,13 +278,22 @@ export async function installTemplate({ packageVersion, }); } + + const composedOfTemplates = await installDatasetComponentTemplates( + templateName, + dataset.elasticsearch, + callCluster + ); + const template = getTemplate({ type: dataset.type, templateName, mappings, pipelineName, packageName, + composedOfTemplates, }); + // TODO: Check return values for errors const callClusterParams: { method: string; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index 73a6767f6b947c..99e568bf771f83 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -29,10 +29,37 @@ test('get template', () => { templateName, packageName: 'nginx', mappings: { properties: {} }, + composedOfTemplates: [], }); expect(template.index_patterns).toStrictEqual([`${templateName}-*`]); }); +test('adds composed_of correctly', () => { + const composedOfTemplates = ['component1', 'component2']; + + const template = getTemplate({ + type: 'logs', + templateName: 'name', + packageName: 'nginx', + mappings: { properties: {} }, + composedOfTemplates, + }); + expect(template.composed_of).toStrictEqual(composedOfTemplates); +}); + +test('adds empty composed_of correctly', () => { + const composedOfTemplates: string[] = []; + + const template = getTemplate({ + type: 'logs', + templateName: 'name', + packageName: 'nginx', + mappings: { properties: {} }, + composedOfTemplates, + }); + expect(template.composed_of).toStrictEqual(composedOfTemplates); +}); + test('tests loading base.yml', () => { const ymlPath = path.join(__dirname, '../../fields/tests/base.yml'); const fieldsYML = readFileSync(ymlPath, 'utf-8'); @@ -45,6 +72,7 @@ test('tests loading base.yml', () => { templateName: 'foo', packageName: 'nginx', mappings, + composedOfTemplates: [], }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -62,6 +90,7 @@ test('tests loading coredns.logs.yml', () => { templateName: 'foo', packageName: 'coredns', mappings, + composedOfTemplates: [], }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -79,6 +108,7 @@ test('tests loading system.yml', () => { templateName: 'whatsthis', packageName: 'system', mappings, + composedOfTemplates: [], }); expect(template).toMatchSnapshot(path.basename(ymlPath)); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 2de378f7175348..e7867532ed1762 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -43,14 +43,16 @@ export function getTemplate({ mappings, pipelineName, packageName, + composedOfTemplates, }: { type: string; templateName: string; mappings: IndexTemplateMappings; pipelineName?: string | undefined; packageName: string; + composedOfTemplates: string[]; }): IndexTemplate { - const template = getBaseTemplate(type, templateName, mappings, packageName); + const template = getBaseTemplate(type, templateName, mappings, packageName, composedOfTemplates); if (pipelineName) { template.template.settings.index.default_pipeline = pipelineName; } @@ -244,7 +246,8 @@ function getBaseTemplate( type: string, templateName: string, mappings: IndexTemplateMappings, - packageName: string + packageName: string, + composedOfTemplates: string[] ): IndexTemplate { return { // This takes precedence over all index templates installed by ES by default (logs-*-* and metrics-*-*) @@ -308,6 +311,7 @@ function getBaseTemplate( data_stream: { timestamp_field: '@timestamp', }, + composed_of: composedOfTemplates, _meta: { package: { name: packageName, diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 8239302a97832d..a559ca18cfedef 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -41,6 +41,7 @@ export { PackageInfo, RegistryVarsEntry, Dataset, + RegistryElasticsearch, AssetReference, ElasticsearchAssetType, IngestAssetType, diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/fields/fields.yml new file mode 100644 index 00000000000000..12a9a03c1337b4 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/manifest.yml new file mode 100644 index 00000000000000..9ac3c68a0be9ec --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/docs/README.md new file mode 100644 index 00000000000000..17fb41ceae242d --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing the that the settings and mappings section get used diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 00000000000000..b03007a76ffcc5 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/manifest.yml new file mode 100644 index 00000000000000..ba9fd0fada006d --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: overrides +title: Mappings Settings Test +description: This is a test package for testing that the mappings and settings sections in the dataset manifest are applied. +version: 0.1.0 +categories: ['security'] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index ef8880f86078b3..3f8df8379e743a 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -11,5 +11,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./file')); //loadTestFile(require.resolve('./template')); loadTestFile(require.resolve('./ilm')); + loadTestFile(require.resolve('./install')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/install.ts b/x-pack/test/ingest_manager_api_integration/apis/install.ts new file mode 100644 index 00000000000000..92078c25419dfd --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/install.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../helpers'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const dockerServers = getService('dockerServers'); + const log = getService('log'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + const mappingsPackage = 'overrides-0.1.0'; + const server = dockerServers.get('registry'); + + describe('installs packages that include settings and mappings overrides', async () => { + after(async () => { + if (server.enabled) { + // remove the package just in case it being installed will affect other tests + await deletePackage(mappingsPackage); + } + }); + + it('should install the overrides package correctly', async function () { + if (server.enabled) { + let { body } = await supertest + .post(`/api/ingest_manager/epm/packages/${mappingsPackage}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + const templateName = body.response[0].id; + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_index_template/${templateName}`, + })); + + // make sure it has the right composed_of array, the contents should be the component templates + // that were installed + expect(body.index_templates[0].index_template.composed_of).to.contain( + `${templateName}-mappings` + ); + expect(body.index_templates[0].index_template.composed_of).to.contain( + `${templateName}-settings` + ); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}-mappings`, + })); + + // Make sure that the `dynamic` field exists and is set to false (as it is in the package) + expect(body.component_templates[0].component_template.template.mappings.dynamic).to.be( + false + ); + // Make sure that the `@timestamp` field exists and is set to date + // this can be removed once https://github.com/elastic/elasticsearch/issues/58956 is resolved + expect( + body.component_templates[0].component_template.template.mappings.properties['@timestamp'] + .type + ).to.be('date'); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}-settings`, + })); + + // Make sure that the lifecycle name gets set correct in the settings + expect( + body.component_templates[0].component_template.template.settings.index.lifecycle.name + ).to.be('reference'); + } else { + warnAndSkipTest(this, log); + } + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/list.ts b/x-pack/test/ingest_manager_api_integration/apis/list.ts index 200358cb6f8f03..abed9a7b859599 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/list.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; const listResponse = await fetchPackageList(); - expect(listResponse.response.length).to.be(11); + expect(listResponse.response.length).to.be(12); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/template.ts b/x-pack/test/ingest_manager_api_integration/apis/template.ts index 8911dd28dc2437..f7e5a894b83ff1 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/template.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/template.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { templateName, mappings, packageName: 'system', + composedOfTemplates: [], }); // This test is not an API integration test with Kibana From a4340f0ecebbc46d77045a83fcc9d1d5cf8fef8b Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jul 2020 15:10:01 -0400 Subject: [PATCH 05/57] [ML] DF Analytics: add ability to edit job for fields supported by API (#70489) * wip: add edit action to dfanalytics table * add update endpoint and edit flyout * show success and error toasts. close flyout and refresh on success * show permission message in edit action * update types * disable update button if mml not valid * show error in toast, init values are config values * fix undefined check for allow lazy start * prevent update if mml is empty --- x-pack/plugins/ml/common/util/validators.ts | 2 + .../data_frame_analytics/common/analytics.ts | 7 +- .../data_frame_analytics/common/index.ts | 1 + .../components/analytics_list/action_edit.tsx | 66 +++++ .../components/analytics_list/actions.tsx | 6 + .../analytics_list/edit_analytics_flyout.tsx | 270 ++++++++++++++++++ .../ml_api_service/data_frame_analytics.ts | 13 +- .../ml/server/client/elasticsearch_ml.ts | 15 + .../ml/server/routes/data_frame_analytics.ts | 40 +++ .../routes/schemas/data_analytics_schema.ts | 6 + 10 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx diff --git a/x-pack/plugins/ml/common/util/validators.ts b/x-pack/plugins/ml/common/util/validators.ts index 5dcdec0553106c..c14c20917a136e 100644 --- a/x-pack/plugins/ml/common/util/validators.ts +++ b/x-pack/plugins/ml/common/util/validators.ts @@ -67,6 +67,8 @@ export function requiredValidator() { export type ValidationResult = object | null; +export type MemoryInputValidatorResult = { invalidUnits: { allowedUnits: string } } | null; + export function memoryInputValidator(allowedUnits = ALLOWED_DATA_UNITS) { return (value: any) => { if (typeof value !== 'string' || value === '') { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 5715687402bcbf..aa637f71db1ccc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -327,9 +327,14 @@ export const isClassificationEvaluateResponse = ( ); }; +export interface UpdateDataFrameAnalyticsConfig { + allow_lazy_start?: string; + description?: string; + model_memory_limit?: string; +} + export interface DataFrameAnalyticsConfig { id: DataFrameAnalyticsId; - // Description attribute is not supported yet description?: string; dest: { index: IndexName; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 58343e26153ccf..65531009e4436b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -13,6 +13,7 @@ export { useRefreshAnalyticsList, DataFrameAnalyticsId, DataFrameAnalyticsConfig, + UpdateDataFrameAnalyticsConfig, IndexName, IndexPattern, REFRESH_ANALYTICS_LIST_STATE, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx new file mode 100644 index 00000000000000..041b52d0322c4e --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx @@ -0,0 +1,66 @@ +/* + * 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, { useState, FC } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; + +import { checkPermission } from '../../../../../capabilities/check_capabilities'; +import { DataFrameAnalyticsListRow } from './common'; + +import { EditAnalyticsFlyout } from './edit_analytics_flyout'; + +interface EditActionProps { + item: DataFrameAnalyticsListRow; +} + +export const EditAction: FC = ({ item }) => { + const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const closeFlyout = () => setIsFlyoutVisible(false); + const showFlyout = () => setIsFlyoutVisible(true); + + const buttonEditText = i18n.translate('xpack.ml.dataframe.analyticsList.editActionName', { + defaultMessage: 'Edit', + }); + + const editButton = ( + + {buttonEditText} + + ); + + if (!canCreateDataFrameAnalytics) { + return ( + + {editButton} + + ); + } + + return ( + <> + {editButton} + {isFlyoutVisible && } + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index b47b23f668530f..b03a3a4c4edb21 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -27,6 +27,7 @@ import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } import { stopAnalytics } from '../../services/analytics_service'; import { StartAction } from './action_start'; +import { EditAction } from './action_edit'; import { DeleteAction } from './action_delete'; interface Props { @@ -133,6 +134,11 @@ export const getActions = ( return stopButton; }, }, + { + render: (item: DataFrameAnalyticsListRow) => { + return ; + }, + }, { render: (item: DataFrameAnalyticsListRow) => { return ; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx new file mode 100644 index 00000000000000..b6aed9321e4e36 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx @@ -0,0 +1,270 @@ +/* + * 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, { FC, useEffect, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiFormRow, + EuiOverlayMask, + EuiSelect, + EuiTitle, +} from '@elastic/eui'; + +import { useMlKibana } from '../../../../../contexts/kibana'; +import { ml } from '../../../../../services/ml_api_service'; +import { + memoryInputValidator, + MemoryInputValidatorResult, +} from '../../../../../../../common/util/validators'; +import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common'; +import { + useRefreshAnalyticsList, + UpdateDataFrameAnalyticsConfig, +} from '../../../../common/analytics'; + +interface EditAnalyticsJobFlyoutProps { + closeFlyout: () => void; + item: DataFrameAnalyticsListRow; +} + +let mmLValidator: (value: any) => MemoryInputValidatorResult; + +export const EditAnalyticsFlyout: FC = ({ closeFlyout, item }) => { + const { id: jobId, config } = item; + const { state } = item.stats; + const initialAllowLazyStart = + config.allow_lazy_start !== undefined ? String(config.allow_lazy_start) : ''; + + const [allowLazyStart, setAllowLazyStart] = useState(initialAllowLazyStart); + const [description, setDescription] = useState(config.description || ''); + const [modelMemoryLimit, setModelMemoryLimit] = useState(config.model_memory_limit); + const [mmlValidationError, setMmlValidationError] = useState(); + + const { + services: { notifications }, + } = useMlKibana(); + const { refresh } = useRefreshAnalyticsList(); + + // Disable if mml is not valid + const updateButtonDisabled = mmlValidationError !== undefined; + + useEffect(() => { + if (mmLValidator === undefined) { + mmLValidator = memoryInputValidator(); + } + // validate mml and create validation message + if (modelMemoryLimit !== '') { + const validationResult = mmLValidator(modelMemoryLimit); + if (validationResult !== null && validationResult.invalidUnits) { + setMmlValidationError( + i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError', { + defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}', + values: { str: validationResult.invalidUnits.allowedUnits }, + }) + ); + } else { + setMmlValidationError(undefined); + } + } else { + setMmlValidationError( + i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryEmptyError', { + defaultMessage: 'Model memory limit must not be empty', + }) + ); + } + }, [modelMemoryLimit]); + + const onSubmit = async () => { + const updateConfig: UpdateDataFrameAnalyticsConfig = Object.assign( + { + allow_lazy_start: allowLazyStart, + description, + }, + modelMemoryLimit && { model_memory_limit: modelMemoryLimit } + ); + + try { + await ml.dataFrameAnalytics.updateDataFrameAnalytics(jobId, updateConfig); + notifications.toasts.addSuccess( + i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutSuccessMessage', { + defaultMessage: 'Analytics job {jobId} has been updated.', + values: { jobId }, + }) + ); + refresh(); + closeFlyout(); + } catch (e) { + // eslint-disable-next-line + console.error(e); + + notifications.toasts.addDanger({ + title: i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutErrorMessage', { + defaultMessage: 'Could not save changes to analytics job {jobId}', + values: { + jobId, + }, + }), + text: extractErrorMessage(e), + }); + } + }; + + return ( + + + + +

+ {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', { + defaultMessage: 'Edit {jobId}', + values: { + jobId, + }, + })} +

+
+
+ + + + ) => + setAllowLazyStart(e.target.value) + } + /> + + + setDescription(e.target.value)} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel', + { + defaultMessage: 'Update the job description.', + } + )} + /> + + + setModelMemoryLimit(e.target.value)} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel', + { + defaultMessage: 'Update the model memory limit.', + } + )} + /> + + + + + + + + {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', { + defaultMessage: 'Cancel', + })} + + + + + {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', { + defaultMessage: 'Update', + })} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 7cdd5478e39835..7de39d91047ef1 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -8,7 +8,10 @@ import { http } from '../http_service'; import { basePath } from './index'; import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common'; +import { + DataFrameAnalyticsConfig, + UpdateDataFrameAnalyticsConfig, +} from '../../data_frame_analytics/common'; import { DeepPartial } from '../../../../common/types/common'; import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../../../common/types/data_frame_analytics'; @@ -72,6 +75,14 @@ export const dataFrameAnalytics = { body, }); }, + updateDataFrameAnalytics(analyticsId: string, updateConfig: UpdateDataFrameAnalyticsConfig) { + const body = JSON.stringify(updateConfig); + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}/_update`, + method: 'POST', + body, + }); + }, evaluateDataFrameAnalytics(evaluateConfig: any) { const body = JSON.stringify(evaluateConfig); return http({ diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts index 07159534e1e2cf..24c80c450f61ab 100644 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts +++ b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts @@ -223,6 +223,21 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'POST', }); + ml.updateDataFrameAnalytics = ca({ + urls: [ + { + fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_update', + req: { + analyticsId: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'POST', + }); + ml.deleteJob = ca({ urls: [ { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index e2601c7ad6a2e7..24be23332e4cf8 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -10,6 +10,7 @@ import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/a import { RouteInitialization } from '../types'; import { dataAnalyticsJobConfigSchema, + dataAnalyticsJobUpdateSchema, dataAnalyticsEvaluateSchema, dataAnalyticsExplainSchema, analyticsIdSchema, @@ -483,6 +484,45 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_update Update specified analytics job + * @apiName UpdateDataFrameAnalyticsJob + * @apiDescription Updates a data frame analytics job. + * + * @apiSchema (params) analyticsIdSchema + */ + router.post( + { + path: '/api/ml/data_frame/analytics/{analyticsId}/_update', + validate: { + params: analyticsIdSchema, + body: dataAnalyticsJobUpdateSchema, + }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const { analyticsId } = request.params; + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.updateDataFrameAnalytics', + { + body: request.body, + analyticsId, + } + ); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup DataFrameAnalytics * diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index e6b4e4ccf85823..5469c2fefdf33a 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -69,6 +69,12 @@ export const deleteDataFrameAnalyticsJobSchema = schema.object({ deleteDestIndexPattern: schema.maybe(schema.boolean()), }); +export const dataAnalyticsJobUpdateSchema = schema.object({ + description: schema.maybe(schema.string()), + model_memory_limit: schema.maybe(schema.string()), + allow_lazy_start: schema.maybe(schema.boolean()), +}); + export const stopsDataFrameAnalyticsJobQuerySchema = schema.object({ force: schema.maybe(schema.boolean()), }); From 7b0e9dfe9a50b27bc724d9645585aee49fc1a719 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 6 Jul 2020 21:25:52 +0200 Subject: [PATCH 06/57] [SIEM] Unskips and fixes 'Detection rules, custom' test (#70693) * unskips and fixes 'Detection rules, custom' test * deletes comment Co-authored-by: Elastic Machine --- .../alerts_detection_rules_custom.spec.ts | 7 +- .../security_solution/cypress/objects/rule.ts | 6 +- .../custom_rule_with_timeline/data.json.gz | Bin 67934 -> 74563 bytes .../custom_rule_with_timeline/mappings.json | 2599 ++++------------- 4 files changed, 524 insertions(+), 2088 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 2a1a2d2c8e1947..51c29c15a8097b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newRule, totalNumberOfPrebuiltRulesInEsArchive } from '../objects/rule'; +import { newRule, totalNumberOfPrebuiltRulesInEsArchiveCustomRule } from '../objects/rule'; import { CUSTOM_RULES_BTN, @@ -64,8 +64,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { ALERTS_URL } from '../urls/navigation'; -// // Skipped as was causing failures on master -describe.skip('Detection rules, custom', () => { +describe('Detection rules, custom', () => { before(() => { esArchiverLoad('custom_rule_with_timeline'); }); @@ -90,7 +89,7 @@ describe.skip('Detection rules, custom', () => { changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1; + const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchiveCustomRule + 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index d750fe212002de..c9d3af57e5e598 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -11,6 +11,8 @@ export const totalNumberOfPrebuiltRules = rawRules.length; export const totalNumberOfPrebuiltRulesInEsArchive = 127; +export const totalNumberOfPrebuiltRulesInEsArchiveCustomRule = 145; + interface Mitre { tactic: string; techniques: string[]; @@ -57,7 +59,7 @@ const mitre2: Mitre = { }; export const newRule: CustomRule = { - customQuery: 'host.name: *', + customQuery: 'host.name: * ', name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -67,7 +69,7 @@ export const newRule: CustomRule = { falsePositivesExamples: ['False1', 'False2'], mitre: [mitre1, mitre2], note: '# test markdown', - timelineId: '352c6110-9ffb-11ea-b3d8-857d6042d9bd', + timelineId: '3270f530-bc84-11ea-b73f-89980a6a1ce7', }; export const machineLearningRule: MachineLearningRule = { diff --git a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/data.json.gz index 3d50451cee39fe8c8f8b49da585473585a812c1d..b3a94c77c11846e2e6d34e2908022b2e943f551b 100644 GIT binary patch literal 74563 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 Z58M!_J$|-*jy<8I@O~(EH^S|5MR~?{>6Tp8 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 diff --git a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json index 693878a88f8995..01a768351e4832 100644 --- a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json @@ -14,36 +14,39 @@ "alert": "7b44fba6773e37c806ce290ea9b7024e", "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", - "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", - "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", "canvas-element": "7390014e1091044523666d97247392fc", "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", "cases": "32aa96a6d3855ddda53010ae2048ac22", "cases-comments": "c2061fb929f585df57425102fa928b4b", "cases-configure": "42711cbb311976c0687853f4c1354572", "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "ae24d22d5986d04124cc6568f771066f", + "config": "c63748b75f39d0c54de12d12c1ccbc20", "dashboard": "d00f614b29a80360e1190193fd333bab", - "epm-packages": "92b4b1899b887b090d01c033f3118a85", + "endpoint:exceptions-artifact": "053713a6b91811c7de078ead17384914", + "endpoint:exceptions-manifest": "67c28185da541c1404e7852d30498cd6", + "epm-packages": "04696e7dba1b9597f7d6ed78a4a76658", "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-actions": "00fe5651ed2da16b7f8159bbf0f7d910", "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", - "fleet-agents": "864760267df6c970f629bd4458506c53", - "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "fleet-agents": "578bbfa81650206927683ebde0c85409", + "fleet-enrollment-api-keys": "451e5c329b3ae9722dc7bc8f5921e05d", "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "ingest-agent-configs": "d9a5cbdce8e937f674a7b376c47a34a1", - "ingest-package-configs": "c0fe6347b0eebcbf421841669e3acd31", - "ingest-outputs": "0e57221778a7153c8292edf154099036", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-configs": "f1e09bc73462386a8c07e9d1997d0688", + "ingest-outputs": "87da6a0e27b3a61ad389fb7a7e2da293", + "ingest-package-configs": "48e8bd97e488008e21c0b5a2367b83ad", "ingest_manager_settings": "c5b0749b4ab03c582efd4c14cb8f132c", "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", - "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", "namespace": "2f4316de49999235636386fe51dc06c1", @@ -67,7 +70,7 @@ "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", - "url": "b675c3be8d76ecf029294d51dc7ec65d", + "url": "c7f66a0df8b1b52f17c28c4adb111105", "visualization": "52d7a13ad68a150c4525b292d23e12cc" } }, @@ -109,145 +112,6 @@ } } }, - "agent_actions": { - "properties": { - "agent_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "data": { - "type": "flattened" - }, - "sent_at": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "agent_configs": { - "properties": { - "datasources": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "text" - }, - "namespace": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "status": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "updated_on": { - "type": "keyword" - } - } - }, - "agent_events": { - "properties": { - "action_id": { - "type": "keyword" - }, - "agent_id": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "data": { - "type": "text" - }, - "message": { - "type": "text" - }, - "payload": { - "type": "text" - }, - "stream_id": { - "type": "keyword" - }, - "subtype": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "agents": { - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "active": { - "type": "boolean" - }, - "config_id": { - "type": "keyword" - }, - "config_newest_revision": { - "type": "integer" - }, - "config_revision": { - "type": "integer" - }, - "current_error_events": { - "type": "text" - }, - "default_api_key": { - "type": "keyword" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "type": "text" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "text" - }, - "version": { - "type": "keyword" - } - } - }, "alert": { "properties": { "actions": { @@ -1264,29 +1128,12 @@ } }, "application_usage_totals": { - "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - } - } + "dynamic": "false", + "type": "object" }, "application_usage_transactional": { + "dynamic": "false", "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - }, "timestamp": { "type": "date" } @@ -1339,6 +1186,38 @@ } } }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, "cases": { "properties": { "closed_at": { @@ -1574,7 +1453,7 @@ } }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { "buildNum": { "type": "keyword" @@ -1635,163 +1514,70 @@ } } }, - "datasources": { + "endpoint:exceptions-artifact": { "properties": { - "config_id": { + "body": { + "type": "binary" + }, + "created": { + "index": false, + "type": "date" + }, + "encoding": { + "index": false, "type": "keyword" }, - "description": { - "type": "text" + "identifier": { + "type": "keyword" }, - "enabled": { - "type": "boolean" + "sha256": { + "type": "keyword" }, - "inputs": { + "size": { + "index": false, + "type": "long" + } + } + }, + "endpoint:exceptions-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed": { "properties": { - "config": { - "type": "flattened" - }, - "enabled": { - "type": "boolean" - }, - "processors": { + "id": { "type": "keyword" }, - "streams": { - "properties": { - "config": { - "type": "flattened" - }, - "dataset": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "processors": { - "type": "keyword" - } - }, - "type": "nested" - }, "type": { "type": "keyword" } }, "type": "nested" }, + "internal": { + "type": "boolean" + }, "name": { "type": "keyword" }, - "namespace": { - "type": "keyword" + "removable": { + "type": "boolean" }, - "output_id": { - "type": "keyword" - }, - "package": { - "properties": { - "name": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "revision": { - "type": "integer" - } - } - }, - "enrollment_api_keys": { - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "binary" - }, - "api_key_id": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - }, - "epm-package": { - "properties": { - "installed": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "internal": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "epm-packages": { - "properties": { - "es_index_patterns": { - "dynamic": "false", - "type": "object" - }, - "installed": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "internal": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "removable": { - "type": "boolean" - }, - "version": { + "version": { "type": "keyword" } } @@ -1874,10 +1660,11 @@ "type": "integer" }, "current_error_events": { + "index": false, "type": "text" }, "default_api_key": { - "type": "keyword" + "type": "binary" }, "default_api_key_id": { "type": "keyword" @@ -1894,6 +1681,9 @@ "local_metadata": { "type": "flattened" }, + "packages": { + "type": "keyword" + }, "shared_id": { "type": "keyword" }, @@ -2026,6 +1816,9 @@ } } }, + "inventoryDefaultView": { + "type": "keyword" + }, "logAlias": { "type": "keyword" }, @@ -2061,6 +1854,9 @@ "metricAlias": { "type": "keyword" }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, "name": { "type": "text" } @@ -2068,9 +1864,6 @@ }, "ingest-agent-configs": { "properties": { - "datasources": { - "type": "keyword" - }, "description": { "type": "text" }, @@ -2081,6 +1874,7 @@ "type": "boolean" }, "monitoring_enabled": { + "index": false, "type": "keyword" }, "name": { @@ -2089,6 +1883,9 @@ "namespace": { "type": "keyword" }, + "package_configs": { + "type": "keyword" + }, "revision": { "type": "integer" }, @@ -2103,6 +1900,35 @@ } } }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, "ingest-package-configs": { "properties": { "config_id": { @@ -2121,6 +1947,7 @@ "type": "boolean" }, "inputs": { + "enabled": false, "properties": { "config": { "type": "flattened" @@ -2128,19 +1955,23 @@ "enabled": { "type": "boolean" }, - "processors": { - "type": "keyword" - }, "streams": { "properties": { - "agent_stream": { + "compiled_stream": { "type": "flattened" }, "config": { "type": "flattened" }, "dataset": { - "type": "keyword" + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } }, "enabled": { "type": "boolean" @@ -2148,9 +1979,6 @@ "id": { "type": "keyword" }, - "processors": { - "type": "keyword" - }, "vars": { "type": "flattened" } @@ -2199,34 +2027,6 @@ } } }, - "ingest-outputs": { - "properties": { - "ca_sha256": { - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "fleet_enroll_password": { - "type": "binary" - }, - "fleet_enroll_username": { - "type": "binary" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, "ingest_manager_settings": { "properties": { "agent_auto_upgrade": { @@ -2387,6 +2187,9 @@ }, "lens": { "properties": { + "description": { + "type": "text" + }, "expression": { "index": false, "type": "keyword" @@ -2420,9 +2223,6 @@ }, "map": { "properties": { - "bounds": { - "type": "geo_shape" - }, "description": { "type": "text" }, @@ -2444,68 +2244,8 @@ } }, "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "indexPatternsWithGeoFieldCount": { - "type": "long" - }, - "indexPatternsWithGeoPointFieldCount": { - "type": "long" - }, - "indexPatternsWithGeoShapeFieldCount": { - "type": "long" - }, - "mapsTotalCount": { - "type": "long" - }, - "settings": { - "properties": { - "showMapVisualizationTypes": { - "type": "boolean" - } - } - }, - "timeCaptured": { - "type": "date" - } - } + "enabled": false, + "type": "object" }, "metrics-explorer-view": { "properties": { @@ -2571,6 +2311,9 @@ } }, "type": "nested" + }, + "source": { + "type": "keyword" } } } @@ -2579,7 +2322,7 @@ "migrationVersion": { "dynamic": "true", "properties": { - "dashboard": { + "alert": { "fields": { "keyword": { "ignore_above": 256, @@ -2588,7 +2331,7 @@ }, "type": "text" }, - "index-pattern": { + "config": { "fields": { "keyword": { "ignore_above": 256, @@ -2597,7 +2340,7 @@ }, "type": "text" }, - "ingest-agent-configs": { + "dashboard": { "fields": { "keyword": { "ignore_above": 256, @@ -2606,7 +2349,7 @@ }, "type": "text" }, - "ingest-package-configs": { + "index-pattern": { "fields": { "keyword": { "ignore_above": 256, @@ -2670,45 +2413,14 @@ "namespaces": { "type": "keyword" }, - "outputs": { + "query": { "properties": { - "api_key": { - "type": "keyword" - }, - "ca_sha256": { - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "fleet_enroll_password": { - "type": "binary" + "description": { + "type": "text" }, - "fleet_enroll_username": { - "type": "binary" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" + "filters": { + "enabled": false, + "type": "object" }, "query": { "properties": { @@ -2784,6 +2496,7 @@ } }, "server": { + "dynamic": "strict", "properties": { "uuid": { "type": "keyword" @@ -3208,6 +2921,9 @@ } } }, + "spaceId": { + "type": "keyword" + }, "telemetry": { "properties": { "allowChangingOptInStatus": { @@ -3424,6 +3140,7 @@ "url": { "fields": { "keyword": { + "ignore_above": 2048, "type": "keyword" } }, @@ -3489,14 +3206,6 @@ }, "agent": { "properties": { - "build": { - "properties": { - "original": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "ephemeral_id": { "ignore_above": 1024, "type": "keyword" @@ -3519,6 +3228,27 @@ } } }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, "client": { "properties": { "address": { @@ -3684,10 +3414,6 @@ "id": { "ignore_above": 1024, "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" } } }, @@ -3715,18 +3441,6 @@ } } }, - "project": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "provider": { "ignore_above": 1024, "type": "keyword" @@ -3737,6 +3451,27 @@ } } }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, "container": { "properties": { "id": { @@ -3949,9 +3684,6 @@ } } }, - "compile_time": { - "type": "date" - }, "hash": { "properties": { "md5": { @@ -3972,53 +3704,6 @@ } } }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "mapped_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "mapped_size": { - "type": "long" - }, "name": { "ignore_above": 1024, "type": "keyword" @@ -4029,10 +3714,6 @@ }, "pe": { "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, "company": { "ignore_above": 1024, "type": "keyword" @@ -4045,10 +3726,6 @@ "ignore_above": 1024, "type": "keyword" }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, "original_file_name": { "ignore_above": 1024, "type": "keyword" @@ -4147,46 +3824,6 @@ } } }, - "endpoint": { - "properties": { - "artifact": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "process": { - "properties": { - "ancestry": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "policy": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, "error": { "properties": { "code": { @@ -4360,9 +3997,6 @@ "ignore_above": 1, "type": "keyword" }, - "entry_modified": { - "type": "double" - }, "extension": { "ignore_above": 1024, "type": "keyword" @@ -4399,352 +4033,114 @@ "ignore_above": 1024, "type": "keyword" }, - "macro": { + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { "properties": { - "code_page": { - "type": "long" - }, - "collection": { - "properties": { - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } + "company": { + "ignore_above": 1024, + "type": "keyword" }, - "errors": { - "properties": { - "count": { - "type": "long" - }, - "error_type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" + "description": { + "ignore_above": 1024, + "type": "keyword" }, - "file_extension": { + "file_version": { "ignore_above": 1024, "type": "keyword" }, - "project_file": { - "properties": { - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" }, - "stream": { - "properties": { - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_code_size": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - } - } - }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { + "product": { "ignore_above": 1024, "type": "keyword" } } }, - "mime_type": { + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "mode": { + "type": { "ignore_above": 1024, "type": "keyword" }, - "mtime": { - "type": "date" - }, - "name": { + "uid": { "ignore_above": 1024, "type": "keyword" - }, - "owner": { + } + } + }, + "geo": { + "properties": { + "city_name": { "ignore_above": 1024, "type": "keyword" }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "continent_name": { "ignore_above": 1024, "type": "keyword" }, - "pe": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "quarantine_path": { + "country_iso_code": { "ignore_above": 1024, "type": "keyword" }, - "quarantine_result": { - "type": "boolean" - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "country_name": { "ignore_above": 1024, "type": "keyword" }, - "temp_file_path": { + "location": { + "type": "geo_point" + }, + "name": { "ignore_above": 1024, "type": "keyword" }, - "type": { + "region_iso_code": { "ignore_above": 1024, "type": "keyword" }, - "uid": { + "region_name": { "ignore_above": 1024, "type": "keyword" - }, - "x509": { - "properties": { - "alternative_names": { - "ignore_above": 1024, - "type": "keyword" - }, - "issuer": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "public_key_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_exponent": { - "doc_values": false, - "index": false, - "type": "long" - }, - "public_key_size": { - "type": "long" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version_number": { - "ignore_above": 1024, - "type": "keyword" - } - } } } }, @@ -4764,6 +4160,26 @@ } } }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "host": { "properties": { "architecture": { @@ -4862,10 +4278,6 @@ "ignore_above": 1024, "type": "keyword" }, - "variant": { - "ignore_above": 1024, - "type": "keyword" - }, "version": { "ignore_above": 1024, "type": "keyword" @@ -4995,10 +4407,6 @@ }, "status_code": { "type": "long" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" } } }, @@ -5008,19 +4416,27 @@ } } }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "labels": { "type": "object" }, "log": { "properties": { - "file": { - "properties": { - "path": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "level": { "ignore_above": 1024, "type": "keyword" @@ -5320,10 +4736,6 @@ "ignore_above": 1024, "type": "keyword" }, - "variant": { - "ignore_above": 1024, - "type": "keyword" - }, "version": { "ignore_above": 1024, "type": "keyword" @@ -5370,21 +4782,61 @@ } } }, - "package": { + "os": { "properties": { - "architecture": { + "family": { "ignore_above": 1024, "type": "keyword" }, - "build_version": { + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "checksum": { + "kernel": { "ignore_above": 1024, "type": "keyword" }, - "description": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { "ignore_above": 1024, "type": "keyword" }, @@ -5424,6 +4876,30 @@ } } }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "process": { "properties": { "args": { @@ -5501,46 +4977,6 @@ } } }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "name": { "fields": { "text": { @@ -5688,10 +5124,6 @@ }, "pe": { "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, "company": { "ignore_above": 1024, "type": "keyword" @@ -5704,10 +5136,6 @@ "ignore_above": 1024, "type": "keyword" }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, "original_file_name": { "ignore_above": 1024, "type": "keyword" @@ -5727,132 +5155,17 @@ "ppid": { "type": "long" }, - "services": { - "ignore_above": 1024, - "type": "keyword" - }, "start": { "type": "date" }, "thread": { "properties": { - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_section": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "protection": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "id": { "type": "long" }, "name": { "ignore_above": 1024, "type": "keyword" - }, - "service": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "start_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "start_address_module": { - "ignore_above": 1024, - "type": "keyword" - }, - "token": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "elevation": { - "type": "boolean" - }, - "elevation_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "uptime": { - "type": "long" } } }, @@ -5866,70 +5179,9 @@ "ignore_above": 1024, "type": "keyword" }, - "token": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "elevation": { - "type": "boolean" - }, - "elevation_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "uptime": { "type": "long" }, - "user": { - "ignore_above": 1024, - "type": "keyword" - }, "working_directory": { "fields": { "text": { @@ -6342,6 +5594,12 @@ }, "rule": { "properties": { + "author": { + "type": "keyword" + }, + "building_block_type": { + "type": "keyword" + }, "created_at": { "type": "date" }, @@ -6378,6 +5636,9 @@ "language": { "type": "keyword" }, + "license": { + "type": "keyword" + }, "max_signals": { "type": "keyword" }, @@ -6399,28 +5660,60 @@ "risk_score": { "type": "keyword" }, - "rule_id": { - "type": "keyword" + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } }, - "saved_id": { + "rule_id": { "type": "keyword" }, - "severity": { + "rule_name_override": { "type": "keyword" }, - "size": { + "saved_id": { "type": "keyword" }, - "tags": { + "severity": { "type": "keyword" }, - "threat": { + "severity_mapping": { "properties": { - "framework": { + "field": { "type": "keyword" }, - "tactic": { - "properties": { + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { "id": { "type": "keyword" }, @@ -6453,6 +5746,9 @@ "timeline_title": { "type": "keyword" }, + "timestamp_override": { + "type": "keyword" + }, "to": { "type": "keyword" }, @@ -6539,674 +5835,53 @@ "type": "keyword" }, "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "target": { - "properties": { - "dll": { - "properties": { - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "compile_time": { - "type": "date" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "mapped_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "mapped_size": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "process": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "services": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_section": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "protection": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "start_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "start_address_module": { - "ignore_above": 1024, - "type": "keyword" - }, - "token": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "elevation": { - "type": "boolean" - }, - "elevation_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "uptime": { - "type": "long" - } - } + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" }, - "title": { + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { "fields": { "text": { "norms": false, @@ -7216,71 +5891,31 @@ "ignore_above": 1024, "type": "keyword" }, - "token": { + "group": { "properties": { "domain": { "ignore_above": 1024, "type": "keyword" }, - "elevation": { - "type": "boolean" - }, - "elevation_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { + "id": { "ignore_above": 1024, "type": "keyword" }, - "user": { + "name": { "ignore_above": 1024, "type": "keyword" } } }, - "uptime": { - "type": "long" + "hash": { + "ignore_above": 1024, + "type": "keyword" }, - "user": { + "id": { "ignore_above": 1024, "type": "keyword" }, - "working_directory": { + "name": { "fields": { "text": { "norms": false, @@ -7294,6 +5929,10 @@ } } }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, "threat": { "properties": { "framework": { @@ -7397,112 +6036,6 @@ "supported_ciphers": { "ignore_above": 1024, "type": "keyword" - }, - "x509": { - "properties": { - "alternative_names": { - "ignore_above": 1024, - "type": "keyword" - }, - "issuer": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "public_key_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_exponent": { - "doc_values": false, - "index": false, - "type": "long" - }, - "public_key_size": { - "type": "long" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version_number": { - "ignore_above": 1024, - "type": "keyword" - } - } } } }, @@ -7563,112 +6096,6 @@ "subject": { "ignore_above": 1024, "type": "keyword" - }, - "x509": { - "properties": { - "alternative_names": { - "ignore_above": 1024, - "type": "keyword" - }, - "issuer": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "public_key_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_exponent": { - "doc_values": false, - "index": false, - "type": "long" - }, - "public_key_size": { - "type": "long" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version_number": { - "ignore_above": 1024, - "type": "keyword" - } - } } } }, @@ -7879,10 +6306,6 @@ "ignore_above": 1024, "type": "keyword" }, - "variant": { - "ignore_above": 1024, - "type": "keyword" - }, "version": { "ignore_above": 1024, "type": "keyword" @@ -7895,6 +6318,18 @@ } } }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "vulnerability": { "properties": { "category": { From cd508994931d26b0c7266b404b287df8a6eb6b98 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 6 Jul 2020 21:26:34 +0200 Subject: [PATCH 07/57] fixes and unskips 'export rule' test (#70699) Co-authored-by: Elastic Machine --- .../alerts_detection_rules_export.spec.ts | 7 +- .../test_files/expected_rules_export.ndjson | 2 +- .../es_archives/export_rule/data.json.gz | Bin 0 -> 28233 bytes .../es_archives/export_rule/mappings.json | 6415 +++++++++++++++++ 4 files changed, 6419 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz create mode 100644 x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index 06e9228de4f490..fdab3016de8de8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -17,10 +17,9 @@ import { ALERTS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -// Skipped as was causing failures on master -describe.skip('Export rules', () => { +describe('Export rules', () => { before(() => { - esArchiverLoad('custom_rules'); + esArchiverLoad('export_rule'); cy.server(); cy.route( 'POST', @@ -29,7 +28,7 @@ describe.skip('Export rules', () => { }); after(() => { - esArchiverUnload('custom_rules'); + esArchiverUnload('export_rule'); }); it('Exports a custom rule', () => { diff --git a/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson index dcbfa9d0dd16ef..7baa59fb3d8c0e 100644 --- a/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson +++ b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson @@ -1,2 +1,2 @@ -{"actions":[],"created_at":"2020-03-26T10:09:07.569Z","updated_at":"2020-03-26T10:09:08.021Z","created_by":"elastic","description":"Rule 1","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"49db5bd1-bdd5-4821-be26-bb70a815dedb","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"0cea4194-03f2-4072-b281-d31b72221d9d","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"name":"Rule 1","query":"host.name:*","references":[],"meta":{"from":"1m","throttle":"no_actions"},"severity":"low","updated_by":"elastic","tags":["rule1"],"to":"now","type":"query","threat":[],"throttle":"no_actions","version":1,"exceptions_list":[]} +{"author":[],"actions":[],"created_at":"2020-07-03T10:44:10.567Z","updated_at":"2020-07-03T10:44:10.941Z","created_by":"elastic","description":"Export rule","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"ad65b1b6-be18-4e41-9d0a-89d8576053d8","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"50a3776b-144d-4cff-9f1f-1173e0d5d4a4","language":"kuery","license":"","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"risk_score_mapping":[],"rule_name_override":"","name":"Export rule","query":"host.name: * ","references":[],"meta":{"from":"1m","kibana_siem_app_url":"http://localhost:5620/app/security"},"severity":"low","severity_mapping":[],"updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[],"throttle":"no_actions","timestamp_override":"","version":1,"exceptions_list":[]} {"exported_count":1,"missing_rules":[],"missing_rules_count":0} diff --git a/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..9501cf5e0586e96a97076fd0d71b5db6485eb6ea GIT binary patch literal 28233 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 Date: Mon, 6 Jul 2020 15:38:21 -0400 Subject: [PATCH 08/57] change user facing text Data streams to datasets (#70840) --- .../ingest_manager/constants/page_paths.ts | 4 ++-- .../ingest_manager/hooks/use_breadcrumbs.tsx | 2 +- .../applications/ingest_manager/layouts/default.tsx | 2 +- .../sections/data_stream/list_page/index.tsx | 10 +++++----- .../overview/components/datastream_section.tsx | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts index 9881d5e40d8ab2..9f1088a94aa946 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -53,7 +53,7 @@ export const PAGE_ROUTING_PATHS = { fleet_agent_details_events: '/fleet/agents/:agentId', fleet_agent_details_details: '/fleet/agents/:agentId/details', fleet_enrollment_tokens: '/fleet/enrollment-tokens', - data_streams: '/data-streams', + data_streams: '/datasets', }; export const pagePathGetters: { @@ -80,5 +80,5 @@ export const pagePathGetters: { fleet_agent_details: ({ agentId, tabId }) => `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', - data_streams: () => '/data-streams', + data_streams: () => '/datasets', }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index 2b92987963ef6a..293638cff50bf1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -207,7 +207,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { text: i18n.translate('xpack.ingestManager.breadcrumbs.datastreamsPageTitle', { - defaultMessage: 'Data streams', + defaultMessage: 'Datasets', }), }, ], diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 5e0cba7383e9ca..1f356301b714ac 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -103,7 +103,7 @@ export const DefaultLayout: React.FunctionComponent = ({ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index e1583d2e426bc2..a6e458a4615cdb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -32,7 +32,7 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => (

@@ -177,7 +177,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {

} @@ -220,14 +220,14 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { isLoading ? ( ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( emptyPrompt ) : ( ) } @@ -257,7 +257,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { placeholder: i18n.translate( 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', { - defaultMessage: 'Filter data streams', + defaultMessage: 'Filter datasets', } ), incremental: true, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx index 87906afb4122ab..eab6cf087e1274 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx @@ -51,14 +51,14 @@ export const OverviewDatastreamSection: React.FC = () => {

@@ -70,7 +70,7 @@ export const OverviewDatastreamSection: React.FC = () => { From 984ea0700ee8b84e69f626792e1dd913607307c9 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 6 Jul 2020 15:46:30 -0400 Subject: [PATCH 09/57] [Ingest Manager ] prepend kibana asset ids with package name (#70502) * prepend asset ids with package name * fix type * cleanup Co-authored-by: Elastic Machine --- .../services/epm/kibana/assets/install.ts | 119 ++++++++++++++++ .../tests/__snapshots__/install.test.ts.snap | 133 ++++++++++++++++++ .../epm/kibana/assets/tests/dashboard.json | 129 +++++++++++++++++ .../epm/kibana/assets/tests/install.test.ts | 35 +++++ .../services/epm/packages/get_objects.ts | 32 ----- .../server/services/epm/packages/index.ts | 2 +- .../server/services/epm/packages/install.ts | 58 +------- 7 files changed, 419 insertions(+), 89 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts new file mode 100644 index 00000000000000..ae6493d4716e81 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -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 { + SavedObject, + SavedObjectsBulkCreateObject, + SavedObjectsClientContract, +} from 'src/core/server'; +import * as Registry from '../../registry'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; + +type SavedObjectToBe = Required & { type: AssetType }; +export type ArchiveAsset = Pick< + SavedObject, + 'id' | 'attributes' | 'migrationVersion' | 'references' +> & { + type: AssetType; +}; + +export async function getKibanaAsset(key: string) { + const buffer = Registry.getAsset(key); + + // cache values are buffers. convert to string / JSON + return JSON.parse(buffer.toString('utf8')); +} + +export function createSavedObjectKibanaAsset( + jsonAsset: ArchiveAsset, + pkgName: string +): SavedObjectToBe { + // convert that to an object + const asset = changeAssetIds(jsonAsset, pkgName); + + return { + type: asset.type, + id: asset.id, + attributes: asset.attributes, + references: asset.references || [], + migrationVersion: asset.migrationVersion || {}, + }; +} + +// modifies id property and the id property of references objects (not index-pattern) +// to be prepended with the package name to distinguish assets from Beats modules' assets +export const changeAssetIds = (asset: ArchiveAsset, pkgName: string): ArchiveAsset => { + const references = asset.references.map((ref) => { + if (ref.type === KibanaAssetType.indexPattern) return ref; + const id = getAssetId(ref.id, pkgName); + return { ...ref, id }; + }); + return { + ...asset, + id: getAssetId(asset.id, pkgName), + references, + }; +}; + +export const getAssetId = (id: string, pkgName: string) => { + return `${pkgName}-${id}`; +}; + +// TODO: make it an exhaustive list +// e.g. switch statement with cases for each enum key returning `never` for default case +export async function installKibanaAssets(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; + paths: string[]; +}) { + const { savedObjectsClient, paths, pkgName } = options; + + // Only install Kibana assets during package installation. + const kibanaAssetTypes = Object.values(KibanaAssetType); + const installationPromises = kibanaAssetTypes.map((assetType) => + installKibanaSavedObjects({ savedObjectsClient, assetType, paths, pkgName }) + ); + + // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] + // call .flat to flatten into one dimensional array + return Promise.all(installationPromises).then((results) => results.flat()); +} + +async function installKibanaSavedObjects({ + savedObjectsClient, + assetType, + paths, + pkgName, +}: { + savedObjectsClient: SavedObjectsClientContract; + assetType: KibanaAssetType; + paths: string[]; + pkgName: string; +}) { + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); + const toBeSavedObjects = await Promise.all( + kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset, pkgName)) + ); + + if (toBeSavedObjects.length === 0) { + return []; + } else { + const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { + overwrite: true, + }); + const createdObjects = createResults.saved_objects; + const installed = createdObjects.map(toAssetReference); + return installed; + } +} + +function toAssetReference({ id, type }: SavedObject) { + const reference: AssetReference = { id, type: type as KibanaAssetType }; + + return reference; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap new file mode 100644 index 00000000000000..638ed4b6118c99 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`a kibana asset id and its reference ids are appended with package name changeAssetIds output matches snapshot: dashboard.json 1`] = ` +{ + "attributes": { + "description": "Overview dashboard for the Nginx integration in Metrics", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": { + "filter": [], + "highlightAll": true, + "query": { + "language": "kuery", + "query": "" + }, + "version": true + } + }, + "optionsJSON": { + "darkTheme": false, + "hidePanelTitles": false, + "useMargins": true + }, + "panelsJSON": [ + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "1", + "w": 24, + "x": 24, + "y": 0 + }, + "panelIndex": "1", + "panelRefName": "panel_0", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "2", + "w": 24, + "x": 24, + "y": 12 + }, + "panelIndex": "2", + "panelRefName": "panel_1", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "3", + "w": 24, + "x": 0, + "y": 12 + }, + "panelIndex": "3", + "panelRefName": "panel_2", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "4", + "w": 24, + "x": 0, + "y": 0 + }, + "panelIndex": "4", + "panelRefName": "panel_3", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "5", + "w": 48, + "x": 0, + "y": 24 + }, + "panelIndex": "5", + "panelRefName": "panel_4", + "version": "7.3.0" + } + ], + "timeRestore": false, + "title": "[Metrics Nginx] Overview ECS", + "version": 1 + }, + "id": "nginx-023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs", + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "metrics-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "nginx-555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_0", + "type": "search" + }, + { + "id": "nginx-a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_1", + "type": "map" + }, + { + "id": "nginx-d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_2", + "type": "dashboard" + }, + { + "id": "nginx-47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "nginx-dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_4", + "type": "visualization" + } + ], + "type": "dashboard" +} +`; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json new file mode 100644 index 00000000000000..e28a61ae5e18c3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json @@ -0,0 +1,129 @@ +{ + "attributes": { + "description": "Overview dashboard for the Nginx integration in Metrics", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": { + "filter": [], + "highlightAll": true, + "query": { + "language": "kuery", + "query": "" + }, + "version": true + } + }, + "optionsJSON": { + "darkTheme": false, + "hidePanelTitles": false, + "useMargins": true + }, + "panelsJSON": [ + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "1", + "w": 24, + "x": 24, + "y": 0 + }, + "panelIndex": "1", + "panelRefName": "panel_0", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "2", + "w": 24, + "x": 24, + "y": 12 + }, + "panelIndex": "2", + "panelRefName": "panel_1", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "3", + "w": 24, + "x": 0, + "y": 12 + }, + "panelIndex": "3", + "panelRefName": "panel_2", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "4", + "w": 24, + "x": 0, + "y": 0 + }, + "panelIndex": "4", + "panelRefName": "panel_3", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "5", + "w": 48, + "x": 0, + "y": 24 + }, + "panelIndex": "5", + "panelRefName": "panel_4", + "version": "7.3.0" + } + ], + "timeRestore": false, + "title": "[Metrics Nginx] Overview ECS", + "version": 1 + }, + "id": "023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs", + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "metrics-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_0", + "type": "search" + }, + { + "id": "a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_1", + "type": "map" + }, + { + "id": "d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_2", + "type": "dashboard" + }, + { + "id": "47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_4", + "type": "visualization" + } + ], + "type": "dashboard" +} \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts new file mode 100644 index 00000000000000..f9bc4cdbf203fd --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.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 { readFileSync } from 'fs'; +import path from 'path'; +import { getAssetId, changeAssetIds } from '../install'; + +expect.addSnapshotSerializer({ + print(val) { + return JSON.stringify(val, null, 2); + }, + + test(val) { + return val; + }, +}); + +describe('a kibana asset id and its reference ids are appended with package name', () => { + const assetPath = path.join(__dirname, './dashboard.json'); + const kibanaAsset = JSON.parse(readFileSync(assetPath, 'utf-8')); + const pkgName = 'nginx'; + const modifiedAssetObject = changeAssetIds(kibanaAsset, pkgName); + + test('changeAssetIds output matches snapshot', () => { + expect(modifiedAssetObject).toMatchSnapshot(path.basename(assetPath)); + }); + + test('getAssetId', () => { + const id = '47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs'; + expect(getAssetId(id, pkgName)).toBe(`${pkgName}-${id}`); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts deleted file mode 100644 index b623295c5e0604..00000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts +++ /dev/null @@ -1,32 +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 { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server'; -import { AssetType } from '../../../types'; -import * as Registry from '../registry'; - -type ArchiveAsset = Pick; -type SavedObjectToBe = Required & { type: AssetType }; - -export async function getObject(key: string) { - const buffer = Registry.getAsset(key); - - // cache values are buffers. convert to string / JSON - const json = buffer.toString('utf8'); - // convert that to an object - const asset: ArchiveAsset = JSON.parse(json); - - const { type, file } = Registry.pathParts(key); - const savedObject: SavedObjectToBe = { - type, - id: file.replace('.json', ''), - attributes: asset.attributes, - references: asset.references || [], - migrationVersion: asset.migrationVersion || {}, - }; - - return savedObject; -} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index b79f9178ad6af4..53ffd5c6e70328 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -23,7 +23,7 @@ export { SearchParams, } from './get'; -export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; +export { installPackage, ensureInstalledPackage } from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 910283549abdfc..8f73bc9a027653 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, Installation, - KibanaAssetType, CallESAsCurrentUser, DefaultPackages, ElasticsearchAssetType, @@ -18,7 +17,7 @@ import { } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { getObject } from './get_objects'; +import { installKibanaAssets } from '../kibana/assets/install'; import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; @@ -121,7 +120,6 @@ export async function installPackage(options: { installKibanaAssets({ savedObjectsClient, pkgName, - pkgVersion, paths, }), installPipelines(registryPackageInfo, paths, callCluster), @@ -185,27 +183,6 @@ export async function installPackage(options: { }); } -// TODO: make it an exhaustive list -// e.g. switch statement with cases for each enum key returning `never` for default case -export async function installKibanaAssets(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - pkgVersion: string; - paths: string[]; -}) { - const { savedObjectsClient, paths } = options; - - // Only install Kibana assets during package installation. - const kibanaAssetTypes = Object.values(KibanaAssetType); - const installationPromises = kibanaAssetTypes.map(async (assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) - ); - - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array - return Promise.all(installationPromises).then((results) => results.flat()); -} - export async function saveInstallationReferences(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; @@ -240,34 +217,3 @@ export async function saveInstallationReferences(options: { return toSaveAssetRefs; } - -async function installKibanaSavedObjects({ - savedObjectsClient, - assetType, - paths, -}: { - savedObjectsClient: SavedObjectsClientContract; - assetType: KibanaAssetType; - paths: string[]; -}) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); - - if (toBeSavedObjects.length === 0) { - return []; - } else { - const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { - overwrite: true, - }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; - } -} - -function toAssetReference({ id, type }: SavedObject) { - const reference: AssetReference = { id, type: type as KibanaAssetType }; - - return reference; -} From ee0653658d17a62242c56adfac5c59fede2f4d66 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Mon, 6 Jul 2020 15:49:14 -0400 Subject: [PATCH 10/57] Remove the legacy Ingest Manager plugin. (#65534) The last thing we were using from it was configuring a static assets directory (which is only use for the EPM Integrations header graphic). This is now provided by platform and is not configurable https://github.com/elastic/kibana/blob/da28df5b154bd8223124b1814f5b350b842c309d/src/core/MIGRATION.md#L1344 Moved the header assets to the new directory & updated the `toAssets` helper --- x-pack/index.js | 10 +--------- x-pack/legacy/plugins/ingest_manager/index.ts | 14 -------------- .../sections/epm/hooks/use_links.tsx | 5 +---- .../assets/illustration_integrations_darkmode.svg | 0 .../assets/illustration_integrations_lightmode.svg | 0 5 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 x-pack/legacy/plugins/ingest_manager/index.ts rename x-pack/plugins/ingest_manager/public/{applications/ingest_manager/sections/epm => }/assets/illustration_integrations_darkmode.svg (100%) rename x-pack/plugins/ingest_manager/public/{applications/ingest_manager/sections/epm => }/assets/illustration_integrations_lightmode.svg (100%) diff --git a/x-pack/index.js b/x-pack/index.js index 2d2e42650cfa7d..66fe05e8f035e7 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -9,15 +9,7 @@ import { monitoring } from './legacy/plugins/monitoring'; import { security } from './legacy/plugins/security'; import { beats } from './legacy/plugins/beats_management'; import { spaces } from './legacy/plugins/spaces'; -import { ingestManager } from './legacy/plugins/ingest_manager'; module.exports = function (kibana) { - return [ - xpackMain(kibana), - monitoring(kibana), - spaces(kibana), - security(kibana), - ingestManager(kibana), - beats(kibana), - ]; + return [xpackMain(kibana), monitoring(kibana), spaces(kibana), security(kibana), beats(kibana)]; }; diff --git a/x-pack/legacy/plugins/ingest_manager/index.ts b/x-pack/legacy/plugins/ingest_manager/index.ts deleted file mode 100644 index 2b20bf16f2400e..00000000000000 --- a/x-pack/legacy/plugins/ingest_manager/index.ts +++ /dev/null @@ -1,14 +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 { resolve } from 'path'; - -export function ingestManager(kibana: any) { - return new kibana.Plugin({ - id: 'ingestManager', - require: ['kibana', 'elasticsearch', 'xpack_main'], - publicDir: resolve(__dirname, '../../../plugins/ingest_manager/public'), - }); -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx index 436163bafcfe4f..a453a7f2e28cb8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx @@ -13,10 +13,7 @@ const removeRelativePath = (relativePath: string): string => export function useLinks() { const { http } = useCore(); return { - toAssets: (path: string) => - http.basePath.prepend( - `/plugins/${PLUGIN_ID}/applications/ingest_manager/sections/epm/assets/${path}` - ), + toAssets: (path: string) => http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/${path}`), toImage: (path: string) => http.basePath.prepend(epmRouteService.getFilePath(path)), toRelativeImage: ({ path, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_integrations_darkmode.svg b/x-pack/plugins/ingest_manager/public/assets/illustration_integrations_darkmode.svg similarity index 100% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_integrations_darkmode.svg rename to x-pack/plugins/ingest_manager/public/assets/illustration_integrations_darkmode.svg diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_integrations_lightmode.svg b/x-pack/plugins/ingest_manager/public/assets/illustration_integrations_lightmode.svg similarity index 100% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_integrations_lightmode.svg rename to x-pack/plugins/ingest_manager/public/assets/illustration_integrations_lightmode.svg From eb84503d8abff3d80a0c8762b405dd815d350914 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 6 Jul 2020 12:56:26 -0700 Subject: [PATCH 11/57] upgrade caniuse-lite database (#70833) Co-authored-by: spalger --- yarn.lock | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index eb1943c5cd00cc..5efea82e84c68b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9550,20 +9550,10 @@ can-use-dom@^0.1.0: resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo= -caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022: - version "1.0.30001022" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001022.tgz#9eeffe580c3a8f110b7b1742dcf06a395885e4c6" - integrity sha512-FjwPPtt/I07KyLPkBQ0g7/XuZg6oUkYBVnPHNj3VHJbOjmmJ/GdSo/GUY6MwINEQvjhP6WZVbX8Tvms8xh0D5A== - -caniuse-lite@^1.0.30001035: - version "1.0.30001036" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001036.tgz#930ea5272010d8bf190d859159d757c0b398caf0" - integrity sha512-jU8CIFIj2oR7r4W+5AKcsvWNVIb6Q6OZE3UsrXrZBHFtreT4YgTeOJtTucp+zSedEpTi3L5wASSP0LYIE3if6w== - -caniuse-lite@^1.0.30001043: - version "1.0.30001079" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001079.tgz#ed3e5225cd9a6850984fdd88bf24ce45d69b9c22" - integrity sha512-2KaYheg0iOY+CMmDuAB3DHehrXhhb4OZU4KBVGDr/YKyYAcpudaiUQ9PJ9rxrPlKEoJ3ATasQ5AN48MqpwS43Q== +caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043: + version "1.0.30001094" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001094.tgz#0b11d02e1cdc201348dbd8e3e57bd9b6ce82b175" + integrity sha512-ufHZNtMaDEuRBpTbqD93tIQnngmJ+oBknjvr0IbFympSdtFpAUFmNv4mVKbb53qltxFx0nK3iy32S9AqkLzUNA== canvas@^2.6.1: version "2.6.1" From 11cfe80020d2fba1ab02ef8517e896744c85e35e Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Mon, 6 Jul 2020 15:33:27 -0500 Subject: [PATCH 12/57] [Metrics UI] Fix a bug in Metric Threshold query filter construction (#70672) Co-authored-by: Elastic Machine --- .../metric_threshold/lib/metric_query.test.ts | 59 +++++++++++++++++++ .../metric_threshold/lib/metric_query.ts | 13 ++-- 2 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts new file mode 100644 index 00000000000000..3ad1031f574e25 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.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 { MetricExpressionParams } from '../types'; +import { getElasticsearchMetricQuery } from './metric_query'; + +describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { + const expressionParams = { + metric: 'system.is.a.good.puppy.dog', + aggType: 'avg', + timeUnit: 'm', + timeSize: 1, + } as MetricExpressionParams; + + const timefield = '@timestamp'; + const groupBy = 'host.doggoname'; + + describe('when passed no filterQuery', () => { + const searchBody = getElasticsearchMetricQuery(expressionParams, timefield, groupBy); + test('includes a range filter', () => { + expect( + searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range')) + ).toBeTruthy(); + }); + + test('includes a metric field filter', () => { + expect(searchBody.query.bool.filter).toMatchObject( + expect.arrayContaining([{ exists: { field: 'system.is.a.good.puppy.dog' } }]) + ); + }); + }); + + describe('when passed a filterQuery', () => { + const filterQuery = + // This is adapted from a real-world query that previously broke alerts + // We want to make sure it doesn't override any existing filters + '{"bool":{"filter":[{"bool":{"filter":[{"bool":{"must_not":[{"bool":{"should":[{"query_string":{"query":"bark*","fields":["host.name^1.0"],"type":"best_fields","default_operator":"or","max_determinized_states":10000,"enable_position_increments":true,"fuzziness":"AUTO","fuzzy_prefix_length":0,"fuzzy_max_expansions":50,"phrase_slop":0,"escape":false,"auto_generate_synonyms_phrase_query":true,"fuzzy_transpositions":true,"boost":1}}],"adjust_pure_negative":true,"minimum_should_match":"1","boost":1}}],"adjust_pure_negative":true,"boost":1}},{"bool":{"must_not":[{"bool":{"should":[{"query_string":{"query":"woof*","fields":["host.name^1.0"],"type":"best_fields","default_operator":"or","max_determinized_states":10000,"enable_position_increments":true,"fuzziness":"AUTO","fuzzy_prefix_length":0,"fuzzy_max_expansions":50,"phrase_slop":0,"escape":false,"auto_generate_synonyms_phrase_query":true,"fuzzy_transpositions":true,"boost":1}}],"adjust_pure_negative":true,"minimum_should_match":"1","boost":1}}],"adjust_pure_negative":true,"boost":1}}],"adjust_pure_negative":true,"boost":1}}],"adjust_pure_negative":true,"boost":1}}'; + + const searchBody = getElasticsearchMetricQuery( + expressionParams, + timefield, + groupBy, + filterQuery + ); + test('includes a range filter', () => { + expect( + searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range')) + ).toBeTruthy(); + }); + + test('includes a metric field filter', () => { + expect(searchBody.query.bool.filter).toMatchObject( + expect.arrayContaining([{ exists: { field: 'system.is.a.good.puppy.dog' } }]) + ); + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 5680035d9d609b..15506a30529c4e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -11,11 +11,11 @@ import { createPercentileAggregation } from './create_percentile_aggregation'; const MINIMUM_BUCKETS = 5; -const getParsedFilterQuery: ( - filterQuery: string | undefined -) => Record | Array> = (filterQuery) => { - if (!filterQuery) return {}; - return JSON.parse(filterQuery).bool; +const getParsedFilterQuery: (filterQuery: string | undefined) => Record | null = ( + filterQuery +) => { + if (!filterQuery) return null; + return JSON.parse(filterQuery); }; export const getElasticsearchMetricQuery = ( @@ -129,9 +129,8 @@ export const getElasticsearchMetricQuery = ( filter: [ ...rangeFilters, ...metricFieldFilters, - ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []), + ...(parsedFilterQuery ? [parsedFilterQuery] : []), ], - ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}), }, }, size: 0, From ad20a17bc6c287e4edba4ef57762818f8996988c Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 6 Jul 2020 22:09:19 +0100 Subject: [PATCH 13/57] skip flaky suite (#70880) --- x-pack/test/functional/apps/security/field_level_security.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 7b22d72885c9d2..20b13ad935f93a 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }) { const log = getService('log'); const PageObjects = getPageObjects(['security', 'settings', 'common', 'discover', 'header']); - describe('field_level_security', () => { + // Skipped as it was failing on ES Promotion: https://github.com/elastic/kibana/issues/70880 + describe.skip('field_level_security', () => { before('initialize tests', async () => { await esArchiver.loadIfNeeded('security/flstest/data'); //( data) await esArchiver.load('security/flstest/kibana'); //(savedobject) From 7debf4dd9f8818cb232df6dc2fbf57a8b6bd1bb8 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 6 Jul 2020 14:12:15 -0700 Subject: [PATCH 14/57] [Ingest Manager] Support limiting integrations on an agent config (#70542) * Add API endpoint and hook for retrieving restricted packages * Filter out restricted packages already in use from list of integrations available for an agent config * Allow list agent configs to optionally return expanded package configs, re * Filter out agent configs which already use the restricted package already from list of agent configs available for an integration * Allow more than 20 agent configs to be shown * Rename restricted to limited; add some common methods to DRY * Add limited package check on server side * Adjust copy wording * Fix typings * Add some package config api integration tests, update es archive mappings * Move test to dockerized integation tests directory; move existing epm tests to their own directory * Remove extra assignPackageConfigs() - already handled in packageConfigService.create() * Review fixes * Fix type, reenabled skipped test * Move new EPM integration test file --- .../ingest_manager/common/constants/routes.ts | 1 + .../ingest_manager/common/services/index.ts | 5 +- .../common/services/limited_package.ts | 23 ++++ .../ingest_manager/common/services/routes.ts | 4 + .../ingest_manager/common/types/models/epm.ts | 1 + .../common/types/rest_spec/agent_config.ts | 4 +- .../common/types/rest_spec/common.ts | 3 +- .../common/types/rest_spec/epm.ts | 5 + .../hooks/use_request/agent_config.ts | 4 +- .../ingest_manager/hooks/use_request/epm.ts | 8 ++ .../step_select_config.tsx | 25 +++- .../step_select_package.tsx | 28 +++- .../ingest_manager/services/index.ts | 4 +- .../ingest_manager/types/index.ts | 2 + .../server/routes/agent_config/handlers.ts | 15 +- .../server/routes/epm/handlers.ts | 35 ++++- .../ingest_manager/server/routes/epm/index.ts | 10 ++ .../routes/package_config/handlers.test.ts | 6 +- .../server/routes/package_config/handlers.ts | 24 +--- .../server/services/agent_config.ts | 44 ++++-- .../server/services/epm/packages/get.ts | 23 ++++ .../server/services/epm/packages/index.ts | 4 +- .../server/services/package_config.ts | 39 +++++- .../ingest_manager/server/services/setup.ts | 8 +- .../server/types/rest_spec/agent_config.ts | 4 +- .../es_archives/fleet/agents/mappings.json | 30 ++-- .../apis/{ => epm}/file.ts | 6 +- .../apis/{ => epm}/ilm.ts | 4 +- .../apis/{ => epm}/install.ts | 4 +- .../apis/{ => epm}/list.ts | 6 +- .../apis/{ => epm}/template.ts | 6 +- .../apis/index.js | 17 ++- .../apis/package_config/create.ts | 130 ++++++++++++++++++ 33 files changed, 429 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/common/services/limited_package.ts rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/file.ts (94%) rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/ilm.ts (89%) rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/install.ts (95%) rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/list.ts (87%) rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/template.ts (88%) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index dad3cdce1a497e..7c3b5a198571c9 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -17,6 +17,7 @@ const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; const EPM_PACKAGES_FILE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`; export const EPM_API_ROUTES = { LIST_PATTERN: EPM_PACKAGES_MANY, + LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`, INFO_PATTERN: EPM_PACKAGES_ONE, INSTALL_PATTERN: EPM_PACKAGES_ONE, DELETE_PATTERN: EPM_PACKAGES_ONE, diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts index a0db7c20747e25..0c91dbbe103545 100644 --- a/x-pack/plugins/ingest_manager/common/services/index.ts +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -3,11 +3,10 @@ * 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 AgentStatusKueryHelper from './agent_status'; - export * from './routes'; +export * as AgentStatusKueryHelper from './agent_status'; export { packageToPackageConfigInputs, packageToPackageConfig } from './package_to_config'; export { storedPackageConfigsToAgentInputs } from './package_configs_to_agent_inputs'; export { configToYaml } from './config_to_yaml'; -export { AgentStatusKueryHelper }; +export { isPackageLimited, doesAgentConfigAlreadyIncludePackage } from './limited_package'; export { decodeCloudId } from './decode_cloud_id'; diff --git a/x-pack/plugins/ingest_manager/common/services/limited_package.ts b/x-pack/plugins/ingest_manager/common/services/limited_package.ts new file mode 100644 index 00000000000000..7ef445d55063c0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/limited_package.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 { PackageInfo, AgentConfig, PackageConfig } from '../types'; + +// Assume packages only ever include 1 config template for now +export const isPackageLimited = (packageInfo: PackageInfo): boolean => { + return packageInfo.config_templates?.[0]?.multiple === false; +}; + +export const doesAgentConfigAlreadyIncludePackage = ( + agentConfig: AgentConfig, + packageName: string +): boolean => { + if (agentConfig.package_configs.length && typeof agentConfig.package_configs[0] === 'string') { + throw new Error('Unable to read full package config information'); + } + return (agentConfig.package_configs as PackageConfig[]) + .map((packageConfig) => packageConfig.package?.name || '') + .includes(packageName); +}; diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 463a18887174c9..49de9a4d8fd854 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -27,6 +27,10 @@ export const epmRouteService = { return EPM_API_ROUTES.LIST_PATTERN; }, + getListLimitedPath: () => { + return EPM_API_ROUTES.LIMITED_LIST_PATTERN; + }, + getInfoPath: (pkgkey: string) => { return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey); }, diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 0d2825f0aa80dd..23e31227cbf3c8 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -79,6 +79,7 @@ export interface RegistryConfigTemplate { title: string; description: string; inputs: RegistryInput[]; + multiple?: boolean; } export interface RegistryInput { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts index 86020cb5235ae6..4e1612d144edef 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts @@ -7,7 +7,9 @@ import { AgentConfig, NewAgentConfig, FullAgentConfig } from '../models'; import { ListWithKuery } from './common'; export interface GetAgentConfigsRequest { - query: ListWithKuery; + query: ListWithKuery & { + full?: boolean; + }; } export type GetAgentConfigsResponseItem = AgentConfig & { agents?: number }; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts index 0d1f72afa16f1b..a454e39c203ed6 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { HttpFetchQuery } from 'src/core/public'; -export interface ListWithKuery { +export interface ListWithKuery extends HttpFetchQuery { page?: number; perPage?: number; sortField?: string; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 5ac7fe9e2779b5..c5035d2d444322 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -34,6 +34,11 @@ export interface GetPackagesResponse { success: boolean; } +export interface GetLimitedPackagesResponse { + response: string[]; + success: boolean; +} + export interface GetFileRequest { params: { pkgkey: string; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts index c81303de3d7c3e..56b78c6faa93a9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts @@ -3,7 +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. */ -import { HttpFetchQuery } from 'src/core/public'; import { useRequest, sendRequest, @@ -12,6 +11,7 @@ import { } from './use_request'; import { agentConfigRouteService } from '../../services'; import { + GetAgentConfigsRequest, GetAgentConfigsResponse, GetOneAgentConfigResponse, GetFullAgentConfigResponse, @@ -25,7 +25,7 @@ import { DeleteAgentConfigResponse, } from '../../types'; -export const useGetAgentConfigs = (query: HttpFetchQuery = {}) => { +export const useGetAgentConfigs = (query?: GetAgentConfigsRequest['query']) => { return useRequest({ path: agentConfigRouteService.getListPath(), method: 'get', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts index 128ef8de68aae5..64bee1763b08b4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts @@ -10,6 +10,7 @@ import { epmRouteService } from '../../services'; import { GetCategoriesResponse, GetPackagesResponse, + GetLimitedPackagesResponse, GetInfoResponse, InstallPackageResponse, DeletePackageResponse, @@ -30,6 +31,13 @@ export const useGetPackages = (query: HttpFetchQuery = {}) => { }); }; +export const useGetLimitedPackages = () => { + return useRequest({ + path: epmRouteService.getListLimitedPath(), + method: 'get', + }); +}; + export const useGetPackageInfoByKey = (pkgkey: string) => { return useRequest({ path: epmRouteService.getInfoPath(pkgkey), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx index 849d7bfc63f349..f6391cf1fa4562 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx @@ -9,6 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer, EuiTextColor } from '@elastic/eui'; import { Error } from '../../../components'; import { AgentConfig, PackageInfo, GetAgentConfigsResponseItem } from '../../../types'; +import { isPackageLimited, doesAgentConfigAlreadyIncludePackage } from '../../../services'; import { useGetPackageInfoByKey, useGetAgentConfigs, sendGetOneAgentConfig } from '../../../hooks'; export const StepSelectConfig: React.FunctionComponent<{ @@ -24,7 +25,12 @@ export const StepSelectConfig: React.FunctionComponent<{ const [selectedConfigError, setSelectedConfigError] = useState(); // Fetch package info - const { data: packageInfoData, error: packageInfoError } = useGetPackageInfoByKey(pkgkey); + const { + data: packageInfoData, + error: packageInfoError, + isLoading: packageInfoLoading, + } = useGetPackageInfoByKey(pkgkey); + const isLimitedPackage = (packageInfoData && isPackageLimited(packageInfoData.response)) || false; // Fetch agent configs info const { @@ -36,6 +42,7 @@ export const StepSelectConfig: React.FunctionComponent<{ perPage: 1000, sortField: 'name', sortOrder: 'asc', + full: true, }); const agentConfigs = agentConfigsData?.items || []; const agentConfigsById = agentConfigs.reduce( @@ -112,12 +119,18 @@ export const StepSelectConfig: React.FunctionComponent<{ searchable allowExclusions={false} singleSelection={true} - isLoading={isAgentConfigsLoading} - options={agentConfigs.map(({ id, name, description }) => { + isLoading={isAgentConfigsLoading || packageInfoLoading} + options={agentConfigs.map((agentConf) => { + const alreadyHasLimitedPackage = + (isLimitedPackage && + packageInfoData && + doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) || + false; return { - label: name, - key: id, - checked: selectedConfigId === id ? 'on' : undefined, + label: agentConf.name, + key: agentConf.id, + checked: selectedConfigId === agentConf.id ? 'on' : undefined, + disabled: alreadyHasLimitedPackage, 'data-test-subj': 'agentConfigItem', }; })} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx index e4f4c976688b19..204b862bd4dc4c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx @@ -8,8 +8,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer } from '@elastic/eui'; import { Error } from '../../../components'; -import { AgentConfig, PackageInfo } from '../../../types'; -import { useGetOneAgentConfig, useGetPackages, sendGetPackageInfoByKey } from '../../../hooks'; +import { AgentConfig, PackageInfo, PackageConfig, GetPackagesResponse } from '../../../types'; +import { + useGetOneAgentConfig, + useGetPackages, + useGetLimitedPackages, + sendGetPackageInfoByKey, +} from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; export const StepSelectPackage: React.FunctionComponent<{ @@ -28,12 +33,27 @@ export const StepSelectPackage: React.FunctionComponent<{ const { data: agentConfigData, error: agentConfigError } = useGetOneAgentConfig(agentConfigId); // Fetch packages info + // Filter out limited packages already part of selected agent config + const [packages, setPackages] = useState([]); const { data: packagesData, error: packagesError, isLoading: isPackagesLoading, } = useGetPackages(); - const packages = packagesData?.response || []; + const { + data: limitedPackagesData, + isLoading: isLimitedPackagesLoading, + } = useGetLimitedPackages(); + useEffect(() => { + if (packagesData?.response && limitedPackagesData?.response && agentConfigData?.item) { + const allPackages = packagesData.response; + const limitedPackages = limitedPackagesData.response; + const usedLimitedPackages = (agentConfigData.item.package_configs as PackageConfig[]) + .map((packageConfig) => packageConfig.package?.name || '') + .filter((pkgName) => limitedPackages.includes(pkgName)); + setPackages(allPackages.filter((pkg) => !usedLimitedPackages.includes(pkg.name))); + } + }, [packagesData, limitedPackagesData, agentConfigData]); // Update parent agent config state useEffect(() => { @@ -101,7 +121,7 @@ export const StepSelectPackage: React.FunctionComponent<{ searchable allowExclusions={false} singleSelection={true} - isLoading={isPackagesLoading} + isLoading={isPackagesLoading || isLimitedPackagesLoading} options={packages.map(({ title, name, version, icons }) => { const pkgkey = `${name}-${version}`; return { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 5dc9026aebdee2..9c3b84d0835b85 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -7,6 +7,7 @@ export { getFlattenedObject } from '../../../../../../../src/core/public'; export { + AgentStatusKueryHelper, agentConfigRouteService, packageConfigRouteService, dataStreamRouteService, @@ -21,5 +22,6 @@ export { packageToPackageConfigInputs, storedPackageConfigsToAgentInputs, configToYaml, - AgentStatusKueryHelper, + isPackageLimited, + doesAgentConfigAlreadyIncludePackage, } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index e28d76cae99554..9cd8a756422969 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -24,6 +24,7 @@ export { // API schema - misc setup, status GetFleetStatusResponse, // API schemas - Agent Config + GetAgentConfigsRequest, GetAgentConfigsResponse, GetAgentConfigsResponseItem, GetOneAgentConfigResponse, @@ -92,6 +93,7 @@ export { ServiceName, GetCategoriesResponse, GetPackagesResponse, + GetLimitedPackagesResponse, GetInfoResponse, InstallPackageResponse, DeletePackageResponse, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index 7b12a076ff041c..110f6b9950829c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -38,8 +38,12 @@ export const getAgentConfigsHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const { full: withPackageConfigs = false, ...restOfQuery } = request.query; try { - const { items, total, page, perPage } = await agentConfigService.list(soClient, request.query); + const { items, total, page, perPage } = await agentConfigService.list(soClient, { + withPackageConfigs, + ...restOfQuery, + }); const body: GetAgentConfigsResponse = { items, total, @@ -103,6 +107,7 @@ export const createAgentConfigHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const withSysMonitoring = request.query.sys_monitoring ?? false; try { @@ -128,15 +133,9 @@ export const createAgentConfigHandler: RequestHandler< if (withSysMonitoring && newSysPackageConfig !== undefined && agentConfig !== undefined) { newSysPackageConfig.config_id = agentConfig.id; newSysPackageConfig.namespace = agentConfig.namespace; - const sysPackageConfig = await packageConfigService.create(soClient, newSysPackageConfig, { + await packageConfigService.create(soClient, callCluster, newSysPackageConfig, { user, }); - - if (sysPackageConfig) { - agentConfig = await agentConfigService.assignPackageConfigs(soClient, agentConfig.id, [ - sysPackageConfig.id, - ]); - } } const body: CreateAgentConfigResponse = { diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index eaf0e1a104b3e7..a50b3b13faeab0 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -5,20 +5,21 @@ */ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, CustomHttpResponseOptions } from 'src/core/server'; -import { - GetPackagesRequestSchema, - GetFileRequestSchema, - GetInfoRequestSchema, - InstallPackageRequestSchema, - DeletePackageRequestSchema, -} from '../../types'; import { GetInfoResponse, InstallPackageResponse, DeletePackageResponse, GetCategoriesResponse, GetPackagesResponse, + GetLimitedPackagesResponse, } from '../../../common'; +import { + GetPackagesRequestSchema, + GetFileRequestSchema, + GetInfoRequestSchema, + InstallPackageRequestSchema, + DeletePackageRequestSchema, +} from '../../types'; import { getCategories, getPackages, @@ -26,6 +27,7 @@ import { getPackageInfo, installPackage, removeInstallation, + getLimitedPackages, } from '../../services/epm/packages'; export const getCategoriesHandler: RequestHandler = async (context, request, response) => { @@ -69,6 +71,25 @@ export const getListHandler: RequestHandler< } }; +export const getLimitedListHandler: RequestHandler = async (context, request, response) => { + try { + const savedObjectsClient = context.core.savedObjects.client; + const res = await getLimitedPackages({ savedObjectsClient }); + const body: GetLimitedPackagesResponse = { + response: res, + success: true, + }; + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + export const getFileHandler: RequestHandler> = async ( context, request, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts index fcf81f9894d5e8..ffaf0ce46c89ad 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts @@ -8,6 +8,7 @@ import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants'; import { getCategoriesHandler, getListHandler, + getLimitedListHandler, getFileHandler, getInfoHandler, installPackageHandler, @@ -40,6 +41,15 @@ export const registerRoutes = (router: IRouter) => { getListHandler ); + router.get( + { + path: EPM_API_ROUTES.LIMITED_LIST_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + getLimitedListHandler + ); + router.get( { path: EPM_API_ROUTES.FILEPATH_PATTERN, diff --git a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts index 6d712ce063290d..85ecc5027d64d6 100644 --- a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts @@ -25,7 +25,7 @@ jest.mock('../../services/package_config', (): { assignPackageStream: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)), buildPackageConfigFromPackage: jest.fn(), bulkCreate: jest.fn(), - create: jest.fn((soClient, newData) => + create: jest.fn((soClient, callCluster, newData) => Promise.resolve({ ...newData, id: '1', @@ -213,7 +213,7 @@ describe('When calling package config', () => { const request = getCreateKibanaRequest(); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(packageConfigServiceMock.create.mock.calls[0][1]).toEqual({ + expect(packageConfigServiceMock.create.mock.calls[0][2]).toEqual({ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', description: '', enabled: true, @@ -294,7 +294,7 @@ describe('When calling package config', () => { const request = getCreateKibanaRequest(); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(packageConfigServiceMock.create.mock.calls[0][1]).toEqual({ + expect(packageConfigServiceMock.create.mock.calls[0][2]).toEqual({ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', description: '', enabled: true, diff --git a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts index f11275c92bb689..6b0c2fe9c2ff7c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts @@ -7,7 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import Boom from 'boom'; import { RequestHandler } from 'src/core/server'; import { appContextService, packageConfigService } from '../../services'; -import { ensureInstalledPackage, getPackageInfo } from '../../services/epm/packages'; +import { getPackageInfo } from '../../services/epm/packages'; import { GetPackageConfigsRequestSchema, GetOnePackageConfigRequestSchema, @@ -106,26 +106,10 @@ export const createPackageConfigHandler: RequestHandler< newData = updatedNewData; } - // Make sure the associated package is installed - if (newData.package?.name) { - await ensureInstalledPackage({ - savedObjectsClient: soClient, - pkgName: newData.package.name, - callCluster, - }); - const pkgInfo = await getPackageInfo({ - savedObjectsClient: soClient, - pkgName: newData.package.name, - pkgVersion: newData.package.version, - }); - newData.inputs = (await packageConfigService.assignPackageStream( - pkgInfo, - newData.inputs - )) as TypeOf['inputs']; - } - // Create package config - const packageConfig = await packageConfigService.create(soClient, newData, { user }); + const packageConfig = await packageConfigService.create(soClient, callCluster, newData, { + user, + }); const body: CreatePackageConfigResponse = { item: packageConfig, success: true }; return response.ok({ body, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index bd00727714c334..fe247d5b91db0d 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -141,11 +141,20 @@ class AgentConfigService { public async list( soClient: SavedObjectsClientContract, - options: ListWithKuery + options: ListWithKuery & { + withPackageConfigs?: boolean; + } ): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> { - const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; - - const agentConfigs = await soClient.find({ + const { + page = 1, + perPage = 20, + sortField = 'updated_at', + sortOrder = 'desc', + kuery, + withPackageConfigs = false, + } = options; + + const agentConfigsSO = await soClient.find({ type: SAVED_OBJECT_TYPE, sortField, sortOrder, @@ -160,12 +169,29 @@ class AgentConfigService { : undefined, }); + const agentConfigs = await Promise.all( + agentConfigsSO.saved_objects.map(async (agentConfigSO) => { + const agentConfig = { + id: agentConfigSO.id, + ...agentConfigSO.attributes, + }; + if (withPackageConfigs) { + const agentConfigWithPackageConfigs = await this.get( + soClient, + agentConfigSO.id, + withPackageConfigs + ); + if (agentConfigWithPackageConfigs) { + agentConfig.package_configs = agentConfigWithPackageConfigs.package_configs; + } + } + return agentConfig; + }) + ); + return { - items: agentConfigs.saved_objects.map((agentConfigSO) => ({ - id: agentConfigSO.id, - ...agentConfigSO.attributes, - })), - total: agentConfigs.total, + items: agentConfigs, + total: agentConfigsSO.total, page, perPage, }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index a261eec899d7c2..ad9635cc02e06c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -5,6 +5,7 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; +import { isPackageLimited } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; @@ -49,6 +50,28 @@ export async function getPackages( return packageList; } +// Get package names for packages which cannot have more than one package config on an agent config +// Assume packages only export one config template for now +export async function getLimitedPackages(options: { + savedObjectsClient: SavedObjectsClientContract; +}): Promise { + const { savedObjectsClient } = options; + const allPackages = await getPackages({ savedObjectsClient }); + const installedPackages = allPackages.filter( + (pkg) => (pkg.status = InstallationStatus.installed) + ); + const installedPackagesInfo = await Promise.all( + installedPackages.map((pkgInstall) => { + return getPackageInfo({ + savedObjectsClient, + pkgName: pkgInstall.name, + pkgVersion: pkgInstall.version, + }); + }) + ); + return installedPackagesInfo.filter((pkgInfo) => isPackageLimited).map((pkgInfo) => pkgInfo.name); +} + export async function getPackageSavedObjects(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient.find({ type: PACKAGES_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 53ffd5c6e70328..57c4f77432455d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -11,8 +11,7 @@ import { Installation, InstallationStatus, KibanaAssetType, -} from '../../../../common/types/models/epm'; - +} from '../../../types'; export { getCategories, getFile, @@ -20,6 +19,7 @@ export { getInstallation, getPackageInfo, getPackages, + getLimitedPackages, SearchParams, } from './get'; diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts index 9fa51d025ad2b0..9433a81e74b071 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -7,24 +7,27 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { DeletePackageConfigsResponse, - packageToPackageConfig, PackageConfigInput, PackageConfigInputStream, PackageInfo, + ListWithKuery, + packageToPackageConfig, + isPackageLimited, + doesAgentConfigAlreadyIncludePackage, } from '../../common'; import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../constants'; import { NewPackageConfig, UpdatePackageConfig, PackageConfig, - ListWithKuery, PackageConfigSOAttributes, RegistryPackage, + CallESAsCurrentUser, } from '../types'; import { agentConfigService } from './agent_config'; import { outputService } from './output'; import * as Registry from './epm/registry'; -import { getPackageInfo, getInstallation } from './epm/packages'; +import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; import { getAssetsData } from './epm/packages/assets'; import { createStream } from './epm/agent/agent'; @@ -37,9 +40,39 @@ function getDataset(st: string) { class PackageConfigService { public async create( soClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser, packageConfig: NewPackageConfig, options?: { id?: string; user?: AuthenticatedUser } ): Promise { + // Make sure the associated package is installed + if (packageConfig.package?.name) { + const [, pkgInfo] = await Promise.all([ + ensureInstalledPackage({ + savedObjectsClient: soClient, + pkgName: packageConfig.package.name, + callCluster, + }), + getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packageConfig.package.name, + pkgVersion: packageConfig.package.version, + }), + ]); + + // Check if it is a limited package, and if so, check that the corresponding agent config does not + // already contain a package config for this package + if (isPackageLimited(pkgInfo)) { + const agentConfig = await agentConfigService.get(soClient, packageConfig.config_id, true); + if (agentConfig && doesAgentConfigAlreadyIncludePackage(agentConfig, pkgInfo.name)) { + throw new Error( + `Unable to create package config. Package '${pkgInfo.name}' already exists on this agent config.` + ); + } + } + + packageConfig.inputs = await this.assignPackageStream(pkgInfo, packageConfig.inputs); + } + const isoDate = new Date().toISOString(); const newSo = await soClient.create( SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 61e1d0ad94db8c..e5ed5c589389cb 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -113,6 +113,7 @@ export async function setupIngestManager( if (!isInstalled) { await addPackageToConfig( soClient, + callCluster, installedPackage, configWithPackageConfigs, defaultOutput @@ -192,6 +193,7 @@ function generateRandomPassword() { async function addPackageToConfig( soClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser, packageToInstall: Installation, config: AgentConfig, defaultOutput: Output @@ -208,10 +210,6 @@ async function addPackageToConfig( defaultOutput.id, config.namespace ); - newPackageConfig.inputs = await packageConfigService.assignPackageStream( - packageInfo, - newPackageConfig.inputs - ); - await packageConfigService.create(soClient, newPackageConfig); + await packageConfigService.create(soClient, callCluster, newPackageConfig); } diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts index 306aefb0d51ff5..d076a803f4b532 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts @@ -8,7 +8,9 @@ import { NewAgentConfigSchema } from '../models'; import { ListWithKuerySchema } from './index'; export const GetAgentConfigsRequestSchema = { - query: ListWithKuerySchema, + query: ListWithKuerySchema.extends({ + full: schema.maybe(schema.boolean()), + }), }; export const GetOneAgentConfigRequestSchema = { diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 0b84514de23f2c..1f0aa2f24d6df2 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -1839,6 +1839,12 @@ "config_id": { "type": "keyword" }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, "description": { "type": "text" }, @@ -1847,6 +1853,7 @@ }, "inputs": { "type": "nested", + "enabled": false, "properties": { "config": { "type": "flattened" @@ -1854,20 +1861,24 @@ "enabled": { "type": "boolean" }, - "processors": { - "type": "keyword" - }, "streams": { "type": "nested", "properties": { - "agent_stream": { + "compiled_stream": { "type": "flattened" }, "config": { "type": "flattened" }, "dataset": { - "type": "keyword" + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } }, "enabled": { "type": "boolean" @@ -1875,9 +1886,6 @@ "id": { "type": "keyword" }, - "processors": { - "type": "keyword" - }, "vars": { "type": "flattened" } @@ -1915,6 +1923,12 @@ }, "revision": { "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" } } }, diff --git a/x-pack/test/ingest_manager_api_integration/apis/file.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts similarity index 94% rename from x-pack/test/ingest_manager_api_integration/apis/file.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/file.ts index a7462ac51ecc16..733b8d4fd9bd65 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/file.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../helpers'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; export default function ({ getService }: FtrProviderContext) { const log = getService('log'); @@ -13,7 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const dockerServers = getService('dockerServers'); const server = dockerServers.get('registry'); - describe('package file', () => { + describe('EPM - package file', () => { it('fetches a .png screenshot image', async function () { if (server.enabled) { await supertest diff --git a/x-pack/test/ingest_manager_api_integration/apis/ilm.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/ilm.ts similarity index 89% rename from x-pack/test/ingest_manager_api_integration/apis/ilm.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/ilm.ts index b73a9da5fad594..8a801d59eb5b26 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/ilm.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/ilm.ts @@ -5,10 +5,10 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - describe('ilm', () => { + describe('EPM - ilm', () => { it('setup policy', async () => { const policyName = 'foo'; const es = getService('es'); diff --git a/x-pack/test/ingest_manager_api_integration/apis/install.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts similarity index 95% rename from x-pack/test/ingest_manager_api_integration/apis/install.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/install.ts index 92078c25419dfd..f73ba56c172c49 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/install.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../helpers'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/ingest_manager_api_integration/apis/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts similarity index 87% rename from x-pack/test/ingest_manager_api_integration/apis/list.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/list.ts index abed9a7b859599..1ac1474e03700b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../helpers'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; export default function ({ getService }: FtrProviderContext) { const log = getService('log'); @@ -18,7 +18,7 @@ export default function ({ getService }: FtrProviderContext) { // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions - describe('list', async function () { + describe('EPM - list', async function () { it('lists all packages from the registry', async function () { if (server.enabled) { const fetchPackageList = async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/template.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/template.ts similarity index 88% rename from x-pack/test/ingest_manager_api_integration/apis/template.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/template.ts index f7e5a894b83ff1..c92dac3334de30 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/template.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/template.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { getTemplate } from '../../../plugins/ingest_manager/server/services/epm/elasticsearch/template/template'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { getTemplate } from '../../../../plugins/ingest_manager/server/services/epm/elasticsearch/template/template'; export default function ({ getService }: FtrProviderContext) { const indexPattern = 'foo'; @@ -20,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) { }, }; // This test was inspired by https://github.com/elastic/kibana/blob/master/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js - describe('template', async () => { + describe('EPM - template', async () => { it('can be loaded', async () => { const template = getTemplate({ type: 'logs', diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index 3f8df8379e743a..30c49140c6e2a9 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -5,12 +5,17 @@ */ export default function ({ loadTestFile }) { - describe('EPM Endpoints', function () { + describe('Ingest Manager Endpoints', function () { this.tags('ciGroup7'); - loadTestFile(require.resolve('./list')); - loadTestFile(require.resolve('./file')); - //loadTestFile(require.resolve('./template')); - loadTestFile(require.resolve('./ilm')); - loadTestFile(require.resolve('./install')); + + // EPM + loadTestFile(require.resolve('./epm/list')); + loadTestFile(require.resolve('./epm/file')); + //loadTestFile(require.resolve('./epm/template')); + loadTestFile(require.resolve('./epm/ilm')); + loadTestFile(require.resolve('./epm/install')); + + // Package configs + loadTestFile(require.resolve('./package_config/create')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts new file mode 100644 index 00000000000000..c7748ab255f43c --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; + +export default function ({ getService }: FtrProviderContext) { + const log = getService('log'); + const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + + const server = dockerServers.get('registry'); + // use function () {} and not () => {} here + // because `this` has to point to the Mocha context + // see https://mochajs.org/#arrow-functions + + describe('Package Config - create', async function () { + let agentConfigId: string; + + before(async function () { + const { body: agentConfigResponse } = await supertest + .post(`/api/ingest_manager/agent_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test config', + namespace: 'default', + }); + agentConfigId = agentConfigResponse.item.id; + }); + + it('should work with valid values', async function () { + if (server.enabled) { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/package_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + config_id: agentConfigId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(200); + + expect(apiResponse.success).to.be(true); + } else { + warnAndSkipTest(this, log); + } + }); + + it('should return a 400 with an invalid namespace', async function () { + if (server.enabled) { + await supertest + .post(`/api/ingest_manager/package_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: '', + config_id: agentConfigId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); + } else { + warnAndSkipTest(this, log); + } + }); + + it('should not allow multiple limited packages on the same agent config', async function () { + if (server.enabled) { + await supertest + .post(`/api/ingest_manager/package_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'endpoint-1', + description: '', + namespace: 'default', + config_id: agentConfigId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'endpoint', + title: 'Endpoint', + version: '0.8.0', + }, + }) + .expect(200); + await supertest + .post(`/api/ingest_manager/package_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'endpoint-2', + description: '', + namespace: 'default', + config_id: agentConfigId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'endpoint', + title: 'Endpoint', + version: '0.8.0', + }, + }) + .expect(500); + } else { + warnAndSkipTest(this, log); + } + }); + }); +} From 94a18fda5d3a70dd7ee670dfd93105319935746f Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Mon, 6 Jul 2020 17:51:27 -0400 Subject: [PATCH 15/57] Adding test user to maps functional tests - PR 1 (#70649) adding test user to pr 1 of maps functional tests. --- x-pack/test/functional/apps/maps/discover.js | 12 ++++++ x-pack/test/functional/apps/maps/index.js | 2 +- .../apps/maps/visualize_create_menu.js | 13 +++++- x-pack/test/functional/config.js | 41 +++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js index 5f488d917c182c..8dbd98ed3af2fa 100644 --- a/x-pack/test/functional/apps/maps/discover.js +++ b/x-pack/test/functional/apps/maps/discover.js @@ -9,12 +9,24 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'maps', 'timePicker']); + const security = getService('security'); describe('discover visualize button', () => { beforeEach(async () => { + await security.testUser.setRoles([ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_discover_read', + 'global_visualize_read', + ]); await PageObjects.common.navigateToApp('discover'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should link geo_shape fields to Maps application', async () => { await PageObjects.discover.selectIndexPattern('geo_shapes*'); await PageObjects.discover.clickFieldListItemVisualize('geometry'); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 94c7587decf15e..15928170972d94 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -17,7 +17,7 @@ export default function ({ loadTestFile, getService }) { await esArchiver.load('maps/data'); await esArchiver.load('maps/kibana'); await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); await browser.setWindowSize(1600, 1000); }); diff --git a/x-pack/test/functional/apps/maps/visualize_create_menu.js b/x-pack/test/functional/apps/maps/visualize_create_menu.js index 5a53d3d8b571d9..ef39771d6be075 100644 --- a/x-pack/test/functional/apps/maps/visualize_create_menu.js +++ b/x-pack/test/functional/apps/maps/visualize_create_menu.js @@ -6,14 +6,25 @@ import expect from '@kbn/expect'; -export default function ({ getPageObjects }) { +export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'header', 'maps']); + const security = getService('security'); + describe('visualize create menu', () => { before(async () => { + await security.testUser.setRoles( + ['test_logstash_reader', 'global_maps_all', 'geoshape_data_reader', 'global_visualize_all'], + false + ); + await PageObjects.visualize.navigateToNewVisualization(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show maps application in create menu', async () => { const hasMapsApp = await PageObjects.visualize.hasMapsApp(); expect(hasMapsApp).to.equal(true); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 3eef95b42cb7de..ad65f82d6dfe12 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -230,6 +230,47 @@ export default async function ({ readConfigFile }) { }, ], }, + global_visualize_read: { + kibana: [ + { + feature: { + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }, + global_visualize_all: { + kibana: [ + { + feature: { + visualize: ['all'], + }, + spaces: ['*'], + }, + ], + }, + global_maps_all: { + kibana: [ + { + feature: { + maps: ['all'], + }, + spaces: ['*'], + }, + ], + }, + + geoshape_data_reader: { + elasticsearch: { + indices: [ + { + names: ['geo_shapes*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }, global_devtools_read: { kibana: [ From 2eb0896415122264fe23a76945d969ac74330b52 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 6 Jul 2020 18:07:29 -0400 Subject: [PATCH 16/57] [Ingest Manager] Copy changes (#70828) * update overview page * remove streams column from config table * fleet name chanegs * remove unused component * update translations --- .../create_package_config_page/index.tsx | 2 +- .../package_configs/package_configs_table.tsx | 38 ------------------- .../sections/fleet/agent_list_page/index.tsx | 4 +- .../enrollment_token_list_page/index.tsx | 2 +- .../components/configuration_section.tsx | 6 +-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 7 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx index a81fb232ceaa09..b446e6bf97e7b6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx @@ -314,7 +314,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => { title: i18n.translate( 'xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle', { - defaultMessage: 'Define your integration', + defaultMessage: 'Configure integration', } ), status: !packageInfo || !agentConfig ? 'disabled' : undefined, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx index 42d1075e2ee1fd..4da4e2cc68c9dd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx @@ -10,7 +10,6 @@ import { EuiInMemoryTable, EuiInMemoryTableProps, EuiBadge, - EuiTextColor, EuiContextMenuItem, EuiButton, EuiFlexGroup, @@ -23,7 +22,6 @@ import { useCapabilities, useLink } from '../../../../../hooks'; import { useConfigRefresh } from '../../hooks'; interface InMemoryPackageConfig extends PackageConfig { - streams: { total: number; enabled: number }; inputTypes: string[]; packageName?: string; packageTitle?: string; @@ -72,30 +70,11 @@ export const PackageConfigsTable: React.FunctionComponent = ({ } const dsInputTypes: string[] = []; - const streams = packageConfig.inputs.reduce( - (streamSummary, input) => { - if (!inputTypesValues.includes(input.type)) { - inputTypesValues.push(input.type); - } - if (!dsInputTypes.includes(input.type)) { - dsInputTypes.push(input.type); - } - - streamSummary.total += input.streams.length; - streamSummary.enabled += input.enabled - ? input.streams.filter((stream) => stream.enabled).length - : 0; - - return streamSummary; - }, - { total: 0, enabled: 0 } - ); dsInputTypes.sort(stringSortAscending); return { ...packageConfig, - streams, inputTypes: dsInputTypes, packageName: packageConfig.package?.name ?? '', packageTitle: packageConfig.package?.title ?? '', @@ -175,23 +154,6 @@ export const PackageConfigsTable: React.FunctionComponent = ({ return namespace ? {namespace} : ''; }, }, - { - field: 'streams', - name: i18n.translate( - 'xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle', - { - defaultMessage: 'Streams', - } - ), - render: (streams: InMemoryPackageConfig['streams']) => { - return ( - <> - {streams.enabled} -  / {streams.total} - - ); - }, - }, { name: i18n.translate( 'xpack.ingestManager.configDetails.packageConfigsTable.actionsColumnTitle', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 6d04f63702c641..ec58789becb72e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -245,7 +245,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { { field: 'config_id', name: i18n.translate('xpack.ingestManager.agentList.configColumnTitle', { - defaultMessage: 'Configuration', + defaultMessage: 'Agent config', }), render: (configId: string, agent: Agent) => { const configName = agentConfigs.find((p) => p.id === configId)?.name; @@ -445,7 +445,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { > } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index 800d4abfd45ed4..df0862be9a141d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -175,7 +175,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { { field: 'config_id', name: i18n.translate('xpack.ingestManager.enrollmentTokensList.configTitle', { - defaultMessage: 'Config', + defaultMessage: 'Agent config', }), render: (configId: string) => { const config = agentConfigs.find((c) => c.id === configId); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx index ed4b3fc8e6a5d0..5a5e901d629b5e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx @@ -36,7 +36,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[

@@ -55,7 +55,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ @@ -64,7 +64,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d6f9cd383ae937..c12b1366746b0f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8154,7 +8154,6 @@ "xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle": "データソース", "xpack.ingestManager.configDetails.packageConfigsTable.namespaceColumnTitle": "名前空間", "xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle": "統合", - "xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle": "ストリーム", "xpack.ingestManager.configDetails.subTabs.packageConfigsTabText": "データソース", "xpack.ingestManager.configDetails.subTabs.settingsTabText": "設定", "xpack.ingestManager.configDetails.summary.lastUpdated": "最終更新日:", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 235f8203608d4b..f68a245acbc316 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8158,7 +8158,6 @@ "xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle": "数据源", "xpack.ingestManager.configDetails.packageConfigsTable.namespaceColumnTitle": "命名空间", "xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle": "集成", - "xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle": "流计数", "xpack.ingestManager.configDetails.subTabs.packageConfigsTabText": "数据源", "xpack.ingestManager.configDetails.subTabs.settingsTabText": "设置", "xpack.ingestManager.configDetails.summary.lastUpdated": "最后更新时间", From e35a42aa07b302f89c838f3259c29779486133a1 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 6 Jul 2020 18:14:59 -0400 Subject: [PATCH 17/57] [Component templates] Form wizard (#69732) --- .../component_template_serialization.test.ts | 203 +++++++++++----- .../lib/component_template_serialization.ts | 13 + .../index_management/common/lib/index.ts | 1 + .../public/application/app.tsx | 12 + .../public/application/app_context.tsx | 4 +- .../component_template_create.test.tsx | 218 +++++++++++++++++ .../component_template_edit.test.tsx | 123 ++++++++++ .../component_template_create.helpers.ts | 38 +++ .../component_template_edit.helpers.ts | 38 +++ .../component_template_form.helpers.ts | 159 ++++++++++++ .../helpers/http_requests.ts | 21 +- .../helpers/setup_environment.tsx | 1 + .../component_template_details.tsx | 7 +- .../tab_summary.tsx | 2 +- .../component_template_list.tsx | 45 +++- .../component_template_list/empty_prompt.tsx | 21 +- .../component_template_list/table.tsx | 54 ++++- .../component_template_clone.tsx | 61 +++++ .../component_template_clone/index.ts | 7 + .../component_template_create.tsx | 83 +++++++ .../component_template_create/index.ts | 7 + .../component_template_edit.tsx | 121 +++++++++ .../component_template_edit/index.ts | 7 + .../component_template_form.tsx | 209 ++++++++++++++++ .../component_template_form/index.ts | 7 + .../component_template_form/steps/index.ts | 8 + .../steps/step_logistics.tsx | 229 ++++++++++++++++++ .../steps/step_logistics_container.tsx | 22 ++ .../steps/step_logistics_schema.tsx | 102 ++++++++ .../steps/step_review.tsx | 212 ++++++++++++++++ .../steps/step_review_container.tsx | 24 ++ .../component_template_wizard/index.ts | 11 + .../component_templates_context.tsx | 10 +- .../component_templates/constants.ts | 2 + .../components/component_templates/index.ts | 6 + .../components/component_templates/lib/api.ts | 41 +++- .../component_templates/lib/breadcrumbs.ts | 61 +++++ .../component_templates/lib/documentation.ts | 2 + .../component_templates/lib/index.ts | 4 + .../component_templates/lib/utils.ts | 18 ++ .../component_templates/shared_imports.ts | 36 ++- .../public/application/index.tsx | 3 +- .../application/mount_management_section.ts | 1 + .../routes/api/component_templates/create.ts | 15 +- .../component_templates/schema_validation.ts | 8 +- .../routes/api/component_templates/update.ts | 4 +- .../index_management/component_templates.ts | 17 ++ 47 files changed, 2195 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts index eaa7f24017a2f8..83682f45918e3e 100644 --- a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts +++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts @@ -4,91 +4,164 @@ * you may not use this file except in compliance with the Elastic License. */ -import { deserializeComponentTemplate } from './component_template_serialization'; +import { + deserializeComponentTemplate, + serializeComponentTemplate, +} from './component_template_serialization'; -describe('deserializeComponentTemplate', () => { - test('deserializes a component template', () => { - expect( - deserializeComponentTemplate( - { - name: 'my_component_template', - component_template: { - version: 1, - _meta: { - serialization: { - id: 10, - class: 'MyComponentTemplate', - }, - description: 'set number of shards to one', - }, - template: { - settings: { - number_of_shards: 1, +describe('Component template serialization', () => { + describe('deserializeComponentTemplate()', () => { + test('deserializes a component template', () => { + expect( + deserializeComponentTemplate( + { + name: 'my_component_template', + component_template: { + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', }, - mappings: { - _source: { - enabled: false, + template: { + settings: { + number_of_shards: 1, }, - properties: { - host_name: { - type: 'keyword', + mappings: { + _source: { + enabled: false, }, - created_at: { - type: 'date', - format: 'EEE MMM dd HH:mm:ss Z yyyy', + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, }, }, }, }, }, - }, - [ - { - name: 'my_index_template', - index_template: { - index_patterns: ['foo'], - template: { - settings: { - number_of_replicas: 2, + [ + { + name: 'my_index_template', + index_template: { + index_patterns: ['foo'], + template: { + settings: { + number_of_replicas: 2, + }, }, + composed_of: ['my_component_template'], + }, + }, + ] + ) + ).toEqual({ + name: 'my_component_template', + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', }, - composed_of: ['my_component_template'], }, }, - ] - ) - ).toEqual({ - name: 'my_component_template', - version: 1, - _meta: { - serialization: { - id: 10, - class: 'MyComponentTemplate', }, - description: 'set number of shards to one', - }, - template: { - settings: { - number_of_shards: 1, + _kbnMeta: { + usedBy: ['my_index_template'], }, - mappings: { - _source: { - enabled: false, + }); + }); + }); + + describe('serializeComponentTemplate()', () => { + test('serialize a component template', () => { + expect( + serializeComponentTemplate({ + name: 'my_component_template', + version: 1, + _kbnMeta: { + usedBy: [], + }, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }) + ).toEqual({ + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', }, - properties: { - host_name: { - type: 'keyword', + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, }, - created_at: { - type: 'date', - format: 'EEE MMM dd HH:mm:ss Z yyyy', + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, }, }, }, - }, - _kbnMeta: { - usedBy: ['my_index_template'], - }, + }); }); }); }); diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts index 0db81bf81d3002..672b8140f79fb5 100644 --- a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts @@ -8,6 +8,7 @@ import { ComponentTemplateFromEs, ComponentTemplateDeserialized, ComponentTemplateListItem, + ComponentTemplateSerialized, } from '../types'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; @@ -84,3 +85,15 @@ export function deserializeComponenTemplateList( return componentTemplateListItem; } + +export function serializeComponentTemplate( + componentTemplateDeserialized: ComponentTemplateDeserialized +): ComponentTemplateSerialized { + const { version, template, _meta } = componentTemplateDeserialized; + + return { + version, + template, + _meta, + }; +} diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index 6b1005b4faa05d..f39cc063ba7315 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -20,4 +20,5 @@ export { getTemplateParameter } from './utils'; export { deserializeComponentTemplate, deserializeComponenTemplateList, + serializeComponentTemplate, } from './component_template_serialization'; diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx index 92197bee30c88f..8d78995a94e2f5 100644 --- a/x-pack/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -16,6 +16,11 @@ import { TemplateClone } from './sections/template_clone'; import { TemplateEdit } from './sections/template_edit'; import { useServices } from './app_context'; +import { + ComponentTemplateCreate, + ComponentTemplateEdit, + ComponentTemplateClone, +} from './components'; export const App = ({ history }: { history: ScopedHistory }) => { const { uiMetricService } = useServices(); @@ -34,6 +39,13 @@ export const AppWithoutRouter = () => ( + + + diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index c8219071203736..6fbe177d24e066 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -6,9 +6,10 @@ import React, { createContext, useContext } from 'react'; import { ScopedHistory } from 'kibana/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; - import { CoreStart } from '../../../../../src/core/public'; + import { IngestManagerSetup } from '../../../ingest_manager/public'; import { IndexMgmtMetricsType } from '../types'; import { UiMetricService, NotificationService, HttpService } from './services'; @@ -32,6 +33,7 @@ export interface AppDependencies { notificationService: NotificationService; }; history: ScopedHistory; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx new file mode 100644 index 00000000000000..6c8da4684f019a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx @@ -0,0 +1,218 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from './helpers'; +import { setup, ComponentTemplateCreateTestBed } from './helpers/component_template_create.helpers'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +describe('', () => { + let testBed: ComponentTemplateCreateTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('On component mount', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('should set the correct page header', async () => { + const { exists, find } = testBed; + + // Verify page title + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual('Create component template'); + + // Verify documentation link + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Component Templates docs'); + }); + + describe('Step: Logistics', () => { + test('should toggle the metadata field', async () => { + const { exists, component, actions } = testBed; + + // Meta editor should be hidden by default + // Since the editor itself is mocked, we checked for the mocked element + expect(exists('mockCodeEditor')).toBe(false); + + await act(async () => { + actions.toggleMetaSwitch(); + }); + + component.update(); + + expect(exists('mockCodeEditor')).toBe(true); + }); + + describe('Validation', () => { + test('should require a name', async () => { + const { form, actions, component, find } = testBed; + + await act(async () => { + // Submit logistics step without any values + actions.clickNextButton(); + }); + + component.update(); + + // Verify name is required + expect(form.getErrorsMessages()).toEqual(['A component template name is required.']); + expect(find('nextButton').props().disabled).toEqual(true); + }); + }); + }); + + describe('Step: Review and submit', () => { + const COMPONENT_TEMPLATE_NAME = 'comp-1'; + const SETTINGS = { number_of_shards: 1 }; + const ALIASES = { my_alias: {} }; + + const BOOLEAN_MAPPING_FIELD = { + name: 'boolean_datatype', + type: 'boolean', + }; + + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + const { actions, component } = testBed; + + component.update(); + + // Complete step 1 (logistics) + await actions.completeStepLogistics({ name: COMPONENT_TEMPLATE_NAME }); + + // Complete step 2 (index settings) + await actions.completeStepSettings(SETTINGS); + + // Complete step 3 (mappings) + await actions.completeStepMappings([BOOLEAN_MAPPING_FIELD]); + + // Complete step 4 (aliases) + await actions.completeStepAliases(ALIASES); + }); + + test('should render the review content', () => { + const { find, exists, actions } = testBed; + // Verify page header + expect(exists('stepReview')).toBe(true); + expect(find('stepReview.title').text()).toEqual( + `Review details for '${COMPONENT_TEMPLATE_NAME}'` + ); + + // Verify 2 tabs exist + expect(find('stepReview.content').find('.euiTab').length).toBe(2); + expect( + find('stepReview.content') + .find('.euiTab') + .map((t) => t.text()) + ).toEqual(['Summary', 'Request']); + + // Summary tab should render by default + expect(exists('stepReview.summaryTab')).toBe(true); + expect(exists('stepReview.requestTab')).toBe(false); + + // Navigate to request tab and verify content + actions.selectReviewTab('request'); + + expect(exists('stepReview.summaryTab')).toBe(false); + expect(exists('stepReview.requestTab')).toBe(true); + }); + + test('should send the correct payload when submitting the form', async () => { + const { actions, component } = testBed; + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + name: COMPONENT_TEMPLATE_NAME, + template: { + settings: SETTINGS, + mappings: { + _source: {}, + _meta: {}, + properties: { + [BOOLEAN_MAPPING_FIELD.name]: { + type: BOOLEAN_MAPPING_FIELD.type, + }, + }, + }, + aliases: ALIASES, + }, + _kbnMeta: { usedBy: [] }, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + + test('should surface API errors if the request is unsuccessful', async () => { + const { component, actions, find, exists } = testBed; + + const error = { + status: 409, + error: 'Conflict', + message: `There is already a template with name '${COMPONENT_TEMPLATE_NAME}'`, + }; + + httpRequestsMockHelpers.setCreateComponentTemplateResponse(undefined, { body: error }); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + expect(exists('saveComponentTemplateError')).toBe(true); + expect(find('saveComponentTemplateError').text()).toContain(error.message); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx new file mode 100644 index 00000000000000..f237605756d5c3 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.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 React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from './helpers'; +import { setup, ComponentTemplateEditTestBed } from './helpers/component_template_edit.helpers'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +describe('', () => { + let testBed: ComponentTemplateEditTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + const COMPONENT_TEMPLATE_NAME = 'comp-1'; + const COMPONENT_TEMPLATE_TO_EDIT = { + name: COMPONENT_TEMPLATE_NAME, + template: { + settings: { number_of_shards: 1 }, + }, + _kbnMeta: { usedBy: [] }, + }; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(COMPONENT_TEMPLATE_TO_EDIT); + + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('should set the correct page title', () => { + const { exists, find } = testBed; + + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual( + `Edit component template '${COMPONENT_TEMPLATE_NAME}'` + ); + }); + + it('should set the name field to read only', () => { + const { find } = testBed; + + const nameInput = find('nameField.input'); + expect(nameInput.props().disabled).toEqual(true); + }); + + describe('form payload', () => { + it('should send the correct payload with changed values', async () => { + const { actions, component, form } = testBed; + + await act(async () => { + form.setInputValue('versionField.input', '1'); + actions.clickNextButton(); + }); + + component.update(); + + await actions.completeStepSettings(); + await actions.completeStepMappings(); + await actions.completeStepAliases(); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + version: 1, + ...COMPONENT_TEMPLATE_TO_EDIT, + template: { + ...COMPONENT_TEMPLATE_TO_EDIT.template, + mappings: { + _meta: {}, + _source: {}, + properties: {}, + }, + }, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts new file mode 100644 index 00000000000000..e6ced2fcc309a6 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../../../../test_utils'; +import { BASE_PATH } from '../../../../../../../common'; +import { ComponentTemplateCreate } from '../../../component_template_wizard'; + +import { WithAppDependencies } from './setup_environment'; +import { + getFormActions, + ComponentTemplateFormTestSubjects, +} from './component_template_form.helpers'; + +export type ComponentTemplateCreateTestBed = TestBed & { + actions: ReturnType; +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}/create_component_template`], + componentRoutePath: `${BASE_PATH}/create_component_template`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateCreate), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts new file mode 100644 index 00000000000000..3c0cbb19577a9b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../../../../test_utils'; +import { BASE_PATH } from '../../../../../../../common'; +import { ComponentTemplateEdit } from '../../../component_template_wizard'; + +import { WithAppDependencies } from './setup_environment'; +import { + getFormActions, + ComponentTemplateFormTestSubjects, +} from './component_template_form.helpers'; + +export type ComponentTemplateEditTestBed = TestBed & { + actions: ReturnType; +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}/edit_component_template/comp-1`], + componentRoutePath: `${BASE_PATH}/edit_component_template/:name`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateEdit), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts new file mode 100644 index 00000000000000..f92f46d71e7c79 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts @@ -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 { act } from 'react-dom/test-utils'; + +import { TestBed } from '../../../../../../../../../test_utils'; + +interface MappingField { + name: string; + type: string; +} + +export const getFormActions = (testBed: TestBed) => { + // User actions + const toggleVersionSwitch = () => { + testBed.form.toggleEuiSwitch('versionToggle'); + }; + + const toggleMetaSwitch = () => { + testBed.form.toggleEuiSwitch('metaToggle'); + }; + + const clickNextButton = () => { + testBed.find('nextButton').simulate('click'); + }; + + const clickBackButton = () => { + testBed.find('backButton').simulate('click'); + }; + + const clickSubmitButton = () => { + testBed.find('submitButton').simulate('click'); + }; + + const setMetaField = (jsonString: string) => { + testBed.find('mockCodeEditor').simulate('change', { + jsonString, + }); + }; + + const selectReviewTab = (tab: 'summary' | 'request') => { + const tabs = ['summary', 'request']; + + testBed.find('stepReview.content').find('.euiTab').at(tabs.indexOf(tab)).simulate('click'); + }; + + const completeStepLogistics = async ({ name }: { name: string }) => { + const { form, component } = testBed; + // Add name field + form.setInputValue('nameField.input', name); + + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const completeStepSettings = async (settings?: { [key: string]: any }) => { + const { find, component } = testBed; + + await act(async () => { + if (settings) { + find('mockCodeEditor').simulate('change', { + jsonString: JSON.stringify(settings), + }); // Using mocked EuiCodeEditor + } + + clickNextButton(); + }); + + component.update(); + }; + + const addMappingField = async (name: string, type: string) => { + const { find, form, component } = testBed; + + await act(async () => { + form.setInputValue('nameParameterInput', name); + find('createFieldForm.mockComboBox').simulate('change', [ + { + label: type, + value: type, + }, + ]); + find('createFieldForm.addButton').simulate('click'); + }); + + component.update(); + }; + + const completeStepMappings = async (mappingFields?: MappingField[]) => { + const { component } = testBed; + + if (mappingFields) { + for (const field of mappingFields) { + const { name, type } = field; + await addMappingField(name, type); + } + } + + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const completeStepAliases = async (aliases?: { [key: string]: any }) => { + const { find, component } = testBed; + + await act(async () => { + if (aliases) { + find('mockCodeEditor').simulate('change', { + jsonString: JSON.stringify(aliases), + }); // Using mocked EuiCodeEditor + } + + clickNextButton(); + }); + + component.update(); + }; + + return { + toggleVersionSwitch, + toggleMetaSwitch, + clickNextButton, + clickBackButton, + clickSubmitButton, + setMetaField, + selectReviewTab, + completeStepSettings, + completeStepAliases, + completeStepLogistics, + completeStepMappings, + }; +}; + +export type ComponentTemplateFormTestSubjects = + | 'backButton' + | 'documentationLink' + | 'metaToggle' + | 'metaEditor' + | 'mockCodeEditor' + | 'nameField.input' + | 'nextButton' + | 'pageTitle' + | 'saveComponentTemplateError' + | 'submitButton' + | 'stepReview' + | 'stepReview.title' + | 'stepReview.content' + | 'stepReview.summaryTab' + | 'stepReview.requestTab' + | 'versionField' + | 'versionField.input'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts index b7b674292dd98e..a4e532ba5d3d3a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts @@ -5,7 +5,11 @@ */ import sinon, { SinonFakeServer } from 'sinon'; -import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../../../shared_imports'; +import { + ComponentTemplateListItem, + ComponentTemplateDeserialized, + ComponentTemplateSerialized, +} from '../../../shared_imports'; import { API_BASE_PATH } from './constants'; // Register helpers to mock HTTP Requests @@ -46,10 +50,25 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setCreateComponentTemplateResponse = ( + response?: ComponentTemplateSerialized, + error?: any + ) => { + const status = error ? error.body.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', `${API_BASE_PATH}/component_templates`, [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + return { setLoadComponentTemplatesResponse, setDeleteComponentTemplateResponse, setLoadComponentTemplateResponse, + setCreateComponentTemplateResponse, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index a2194bbfa0186b..70634a226c67b6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -27,6 +27,7 @@ const appDependencies = { trackMetric: () => {}, docLinks: docLinksServiceMock.createStartContract(), toasts: notificationServiceMock.createSetupContract().toasts, + setBreadcrumbs: () => {}, }; export const setupEnvironment = () => { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx index a8007c6363584a..f94c5c38f23ddf 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx @@ -24,6 +24,7 @@ import { useComponentTemplatesContext } from '../component_templates_context'; import { TabSummary } from './tab_summary'; import { ComponentTemplateTabs, TabType } from './tabs'; import { ManageButton, ManageAction } from './manage_button'; +import { attemptToDecodeURI } from '../lib'; interface Props { componentTemplateName: string; @@ -39,8 +40,10 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({ }) => { const { api } = useComponentTemplatesContext(); + const decodedComponentTemplateName = attemptToDecodeURI(componentTemplateName); + const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate( - componentTemplateName + decodedComponentTemplateName ); const [activeTab, setActiveTab] = useState('summary'); @@ -108,7 +111,7 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({

- {componentTemplateName} + {decodedComponentTemplateName}

diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx index 401186f6c962e0..80f28f23c9f91e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx @@ -74,7 +74,7 @@ export const TabSummary: React.FunctionComponent = ({ componentTemplateDe )} {/* Version (optional) */} - {version && ( + {typeof version !== 'undefined' && ( <> = ({ const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]); - const goToList = () => { - return history.push('component_templates'); + const goToComponentTemplateList = () => { + return history.push({ + pathname: 'component_templates', + }); + }; + + const goToEditComponentTemplate = (name: string) => { + return history.push({ + pathname: encodeURI(`edit_component_template/${encodeURIComponent(name)}`), + }); + }; + + const goToCloneComponentTemplate = (name: string) => { + return history.push({ + pathname: encodeURI(`create_component_template/${encodeURIComponent(name)}`), + }); }; // Track component loaded @@ -60,11 +75,13 @@ export const ComponentTemplateList: React.FunctionComponent = ({ componentTemplates={data} onReloadClick={sendRequest} onDeleteClick={setComponentTemplatesToDelete} + onEditClick={goToEditComponentTemplate} + onCloneClick={goToCloneComponentTemplate} history={history as ScopedHistory} /> ); } else if (data && data.length === 0) { - content = ; + content = ; } else if (error) { content = ; } @@ -81,7 +98,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ // refetch the component templates sendRequest(); // go back to list view (if deleted from details flyout) - goToList(); + goToComponentTemplateList(); } setComponentTemplatesToDelete([]); }} @@ -92,9 +109,25 @@ export const ComponentTemplateList: React.FunctionComponent = ({ {/* details flyout */} {componentTemplateName && ( + goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)), + }, + { + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: 'copy', + handleActionClick: () => + goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)), + }, { name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', { defaultMessage: 'Delete', @@ -104,7 +137,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ details._kbnMeta.usedBy.length > 0, closePopoverOnClick: true, handleActionClick: () => { - setComponentTemplatesToDelete([componentTemplateName]); + setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]); }, }, ]} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx index edd9f77cbf635d..fbb1968491ff65 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx @@ -6,11 +6,17 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; import { useComponentTemplatesContext } from '../component_templates_context'; -export const EmptyPrompt: FunctionComponent = () => { +interface Props { + history: RouteComponentProps['history']; +} + +export const EmptyPrompt: FunctionComponent = ({ history }) => { const { documentation } = useComponentTemplatesContext(); return ( @@ -38,6 +44,17 @@ export const EmptyPrompt: FunctionComponent = () => {

} + actions={ + + {i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptButtonLabel', { + defaultMessage: 'Create a component template', + })} + + } /> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index b67a249ae69765..089c2f889e726e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -25,6 +25,8 @@ export interface Props { componentTemplates: ComponentTemplateListItem[]; onReloadClick: () => void; onDeleteClick: (componentTemplateName: string[]) => void; + onEditClick: (componentTemplateName: string) => void; + onCloneClick: (componentTemplateName: string) => void; history: ScopedHistory; } @@ -32,6 +34,8 @@ export const ComponentTable: FunctionComponent = ({ componentTemplates, onReloadClick, onDeleteClick, + onEditClick, + onCloneClick, history, }) => { const { trackMetric } = useComponentTemplatesContext(); @@ -85,6 +89,17 @@ export const ComponentTable: FunctionComponent = ({ defaultMessage: 'Reload', })} , + + {i18n.translate('xpack.idxMgmt.componentTemplatesList.table.createButtonLabel', { + defaultMessage: 'Create a component template', + })} + , ], box: { incremental: true, @@ -135,7 +150,7 @@ export const ComponentTable: FunctionComponent = ({ {...reactRouterNavigate( history, { - pathname: `/component_templates/${name}`, + pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`), }, () => trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS) )} @@ -204,8 +219,37 @@ export const ComponentTable: FunctionComponent = ({ ), actions: [ { - 'data-test-subj': 'deleteComponentTemplateButton', + name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionEditText', { + defaultMessage: 'Edit', + }), + description: i18n.translate( + 'xpack.idxMgmt.componentTemplatesList.table.actionEditDecription', + { + defaultMessage: 'Edit this component template', + } + ), + onClick: ({ name }: ComponentTemplateListItem) => onEditClick(name), isPrimary: true, + icon: 'pencil', + type: 'icon', + 'data-test-subj': 'editComponentTemplateButton', + }, + { + name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionCloneText', { + defaultMessage: 'Clone', + }), + description: i18n.translate( + 'xpack.idxMgmt.componentTemplatesList.table.actionCloneDecription', + { + defaultMessage: 'Clone this component template', + } + ), + onClick: ({ name }: ComponentTemplateListItem) => onCloneClick(name), + icon: 'copy', + type: 'icon', + 'data-test-subj': 'cloneComponentTemplateButton', + }, + { name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.deleteActionLabel', { defaultMessage: 'Delete', }), @@ -213,11 +257,13 @@ export const ComponentTable: FunctionComponent = ({ 'xpack.idxMgmt.componentTemplatesList.table.deleteActionDescription', { defaultMessage: 'Delete this component template' } ), + onClick: ({ name }) => onDeleteClick([name]), + enabled: ({ usedBy }) => usedBy.length === 0, + isPrimary: true, type: 'icon', icon: 'trash', color: 'danger', - onClick: ({ name }) => onDeleteClick([name]), - enabled: ({ usedBy }) => usedBy.length === 0, + 'data-test-subj': 'deleteComponentTemplateButton', }, ], }, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx new file mode 100644 index 00000000000000..94db623f313c79 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.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 React, { FunctionComponent, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SectionLoading } from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { attemptToDecodeURI } from '../../lib'; +import { ComponentTemplateCreate } from '../component_template_create'; + +export interface Params { + sourceComponentTemplateName: string; +} + +export const ComponentTemplateClone: FunctionComponent> = (props) => { + const { sourceComponentTemplateName } = props.match.params; + const decodedSourceName = attemptToDecodeURI(sourceComponentTemplateName); + + const { toasts, api } = useComponentTemplatesContext(); + + const { error, data: componentTemplateToClone, isLoading } = api.useLoadComponentTemplate( + decodedSourceName + ); + + useEffect(() => { + if (error && !isLoading) { + toasts.addError(error, { + title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', { + defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`, + values: { sourceComponentTemplateName }, + }), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading]); + + if (isLoading) { + return ( + + + + ); + } else { + // We still show the create form (unpopulated) even if we were not able to load the + // selected component template data. + const sourceComponentTemplate = componentTemplateToClone + ? { ...componentTemplateToClone, name: `${componentTemplateToClone.name}-copy` } + : undefined; + + return ; + } +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/index.ts new file mode 100644 index 00000000000000..b7165919644f44 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/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 { ComponentTemplateClone } from './component_template_clone'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx new file mode 100644 index 00000000000000..94afadaed37f16 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.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, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { ComponentTemplateDeserialized } from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { ComponentTemplateForm } from '../component_template_form'; + +interface Props { + /** + * This value may be passed in to prepopulate the creation form (e.g., to clone a template) + */ + sourceComponentTemplate?: any; +} + +export const ComponentTemplateCreate: React.FunctionComponent = ({ + history, + sourceComponentTemplate, +}) => { + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const { api, breadcrumbs } = useComponentTemplatesContext(); + + const onSave = async (componentTemplate: ComponentTemplateDeserialized) => { + const { name } = componentTemplate; + + setIsSaving(true); + setSaveError(null); + + const { error } = await api.createComponentTemplate(componentTemplate); + + setIsSaving(false); + + if (error) { + setSaveError(error); + return; + } + + history.push({ + pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`), + }); + }; + + const clearSaveError = () => { + setSaveError(null); + }; + + useEffect(() => { + breadcrumbs.setCreateBreadcrumbs(); + }, [breadcrumbs]); + + return ( + + + +

+ +

+
+ + + + +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/index.ts new file mode 100644 index 00000000000000..6b0e02317888b5 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/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 { ComponentTemplateCreate } from './component_template_create'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx new file mode 100644 index 00000000000000..2bd3dfb34acb9a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -0,0 +1,121 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { ComponentTemplateDeserialized, SectionLoading } from '../../shared_imports'; +import { attemptToDecodeURI } from '../../lib'; +import { ComponentTemplateForm } from '../component_template_form'; + +interface MatchParams { + name: string; +} + +export const ComponentTemplateEdit: React.FunctionComponent> = ({ + match: { + params: { name }, + }, + history, +}) => { + const { api, breadcrumbs } = useComponentTemplatesContext(); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const decodedName = attemptToDecodeURI(name); + + const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(decodedName); + + useEffect(() => { + breadcrumbs.setEditBreadcrumbs(); + }, [breadcrumbs]); + + const onSave = async (updatedComponentTemplate: ComponentTemplateDeserialized) => { + setIsSaving(true); + setSaveError(null); + + const { error: saveErrorObject } = await api.updateComponentTemplate(updatedComponentTemplate); + + setIsSaving(false); + + if (saveErrorObject) { + setSaveError(saveErrorObject); + return; + } + + history.push({ + pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`), + }); + }; + + const clearSaveError = () => { + setSaveError(null); + }; + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="loadComponentTemplateError" + > +
{error.message}
+
+ + + ); + } else if (componentTemplate) { + content = ( + + ); + } + + return ( + + + +

+ +

+
+ + {content} +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/index.ts new file mode 100644 index 00000000000000..1f877bdae24f03 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/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 { ComponentTemplateEdit } from './component_template_edit'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx new file mode 100644 index 00000000000000..6e35fbad31d4e4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx @@ -0,0 +1,209 @@ +/* + * 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, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import { + serializers, + Forms, + ComponentTemplateDeserialized, + CommonWizardSteps, + StepSettingsContainer, + StepMappingsContainer, + StepAliasesContainer, +} from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { StepLogisticsContainer, StepReviewContainer } from './steps'; + +const { stripEmptyFields } = serializers; +const { FormWizard, FormWizardStep } = Forms; + +interface Props { + onSave: (componentTemplate: ComponentTemplateDeserialized) => void; + clearSaveError: () => void; + isSaving: boolean; + saveError: any; + defaultValue?: ComponentTemplateDeserialized; + isEditing?: boolean; +} + +export interface WizardContent extends CommonWizardSteps { + logistics: Omit; +} + +export type WizardSection = keyof WizardContent | 'review'; + +const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { + logistics: { + id: 'logistics', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.logisticsStepName', { + defaultMessage: 'Logistics', + }), + }, + settings: { + id: 'settings', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.settingsStepName', { + defaultMessage: 'Index settings', + }), + }, + mappings: { + id: 'mappings', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.mappingsStepName', { + defaultMessage: 'Mappings', + }), + }, + aliases: { + id: 'aliases', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.aliasesStepName', { + defaultMessage: 'Aliases', + }), + }, + review: { + id: 'review', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.summaryStepName', { + defaultMessage: 'Review', + }), + }, +}; + +export const ComponentTemplateForm = ({ + defaultValue = { + name: '', + template: { + settings: {}, + mappings: {}, + aliases: {}, + }, + _meta: {}, + _kbnMeta: { + usedBy: [], + }, + }, + isEditing, + isSaving, + saveError, + clearSaveError, + onSave, +}: Props) => { + const { + template: { settings, mappings, aliases }, + ...logistics + } = defaultValue; + + const { documentation } = useComponentTemplatesContext(); + + const wizardDefaultValue: WizardContent = { + logistics, + settings, + mappings, + aliases, + }; + + const i18nTexts = { + save: isEditing ? ( + + ) : ( + + ), + }; + + const apiError = saveError ? ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="saveComponentTemplateError" + > +
{saveError.message || saveError.statusText}
+
+ + + ) : null; + + const buildComponentTemplateObject = (initialTemplate: ComponentTemplateDeserialized) => ( + wizardData: WizardContent + ): ComponentTemplateDeserialized => { + const componentTemplate = { + ...initialTemplate, + name: wizardData.logistics.name, + version: wizardData.logistics.version, + _meta: wizardData.logistics._meta, + template: { + settings: wizardData.settings, + mappings: wizardData.mappings, + aliases: wizardData.aliases, + }, + }; + return componentTemplate; + }; + + const onSaveComponentTemplate = useCallback( + async (wizardData: WizardContent) => { + const componentTemplate = buildComponentTemplateObject(defaultValue)(wizardData); + + // This will strip an empty string if "version" is not set, as well as an empty "_meta" object + onSave( + stripEmptyFields(componentTemplate, { + types: ['string', 'object'], + }) as ComponentTemplateDeserialized + ); + + clearSaveError(); + }, + [defaultValue, onSave, clearSaveError] + ); + + return ( + + defaultValue={wizardDefaultValue} + onSave={onSaveComponentTemplate} + isEditing={isEditing} + isSaving={isSaving} + apiError={apiError} + texts={i18nTexts} + > + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/index.ts new file mode 100644 index 00000000000000..84d9a2795ee2c0 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/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 { ComponentTemplateForm } from './component_template_form'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts new file mode 100644 index 00000000000000..b7e3e36e61814d --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/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 { StepLogisticsContainer } from './step_logistics_container'; +export { StepReviewContainer } from './step_review_container'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx new file mode 100644 index 00000000000000..8762eae9d2297c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx @@ -0,0 +1,229 @@ +/* + * 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, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButtonEmpty, + EuiSpacer, + EuiSwitch, + EuiLink, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + useForm, + Form, + getUseField, + getFormRow, + Field, + Forms, + JsonEditorField, +} from '../../../shared_imports'; +import { useComponentTemplatesContext } from '../../../component_templates_context'; +import { logisticsFormSchema } from './step_logistics_schema'; + +const UseField = getUseField({ component: Field }); +const FormRow = getFormRow({ titleTag: 'h3' }); + +interface Props { + defaultValue: { [key: string]: any }; + onChange: (content: Forms.Content) => void; + isEditing?: boolean; +} + +export const StepLogistics: React.FunctionComponent = React.memo( + ({ defaultValue, isEditing, onChange }) => { + const { form } = useForm({ + schema: logisticsFormSchema, + defaultValue, + options: { stripEmptyFields: false }, + }); + + const { documentation } = useComponentTemplatesContext(); + + const [isMetaVisible, setIsMetaVisible] = useState( + Boolean(defaultValue._meta && Object.keys(defaultValue._meta).length) + ); + + const validate = async () => { + return (await form.submit()).isValid; + }; + + useEffect(() => { + onChange({ + isValid: form.isValid, + validate, + getData: form.getFormData, + }); + }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + const subscription = form.subscribe(({ data, isValid }) => { + onChange({ + isValid, + validate, + getData: data.format, + }); + }); + return subscription.unsubscribe; + }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( +
+ + + +

+ +

+
+
+ + + + + + +
+ + + + {/* Name field */} + + } + description={ + + } + > + + + + {/* version field */} + + } + description={ + + } + > + + + + {/* _meta field */} + + } + description={ + <> + + {i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaDocumentionLink', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> + + + + + } + checked={isMetaVisible} + onChange={(e) => setIsMetaVisible(e.target.checked)} + data-test-subj="metaToggle" + /> + + } + > + {isMetaVisible ? ( + + ) : ( + // requires children or a field + // For now, we return an empty
if the editor is not visible +
+ )} + + + ); + } +); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx new file mode 100644 index 00000000000000..d71e36c0d997f4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx @@ -0,0 +1,22 @@ +/* + * 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 { Forms } from '../../../shared_imports'; +import { WizardContent } from '../component_template_form'; +import { StepLogistics } from './step_logistics'; + +interface Props { + isEditing?: boolean; +} + +export const StepLogisticsContainer = ({ isEditing = false }: Props) => { + const { defaultValue, updateContent } = Forms.useContent('logistics'); + + return ( + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx new file mode 100644 index 00000000000000..0c52037abde459 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx @@ -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 React from 'react'; +import { EuiCode } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, fieldValidators, fieldFormatters, FormSchema } from '../../../shared_imports'; + +const { emptyField, containsCharsField, isJsonField } = fieldValidators; +const { toInt } = fieldFormatters; + +const stringifyJson = (json: { [key: string]: any }): string => + Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}'; + +const parseJson = (jsonString: string): object => { + let parsedJSON: any; + + try { + parsedJSON = JSON.parse(jsonString); + } catch { + parsedJSON = {}; + } + + return parsedJSON; +}; + +export const logisticsFormSchema: FormSchema = { + name: { + defaultValue: undefined, + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.nameFieldLabel', { + defaultMessage: 'Name', + }), + type: FIELD_TYPES.TEXT, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.idxMgmt.componentTemplateForm.validation.nameRequiredError', { + defaultMessage: 'A component template name is required.', + }) + ), + }, + { + validator: containsCharsField({ + chars: ' ', + message: i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.nameSpacesError', + { + defaultMessage: 'Spaces are not allowed in a component template name.', + } + ), + }), + }, + ], + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, + _meta: { + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.metaFieldLabel', { + defaultMessage: 'Metadata (optional)', + }), + helpText: ( + {JSON.stringify({ arbitrary_data: 'anything_goes' })}, + }} + /> + ), + serializer: (value) => { + const result = parseJson(value); + // If an empty object was passed, strip out this value entirely. + if (!Object.keys(result).length) { + return undefined; + } + return result; + }, + deserializer: stringifyJson, + validations: [ + { + validator: isJsonField( + i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.metaJsonError', + { + defaultMessage: 'The input is not valid.', + } + ), + { allowEmptyString: true } + ), + }, + ], + }, +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx new file mode 100644 index 00000000000000..ce85854dc79ab6 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx @@ -0,0 +1,212 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiTitle, + EuiFlexItem, + EuiSpacer, + EuiTabbedContent, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiText, + EuiCodeBlock, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + ComponentTemplateDeserialized, + serializers, + serializeComponentTemplate, +} from '../../../shared_imports'; + +const { stripEmptyFields } = serializers; + +const getDescriptionText = (data: any) => { + const hasEntries = data && Object.entries(data).length > 0; + + return hasEntries ? ( + + ) : ( + + ); +}; + +interface Props { + componentTemplate: ComponentTemplateDeserialized; +} + +export const StepReview: React.FunctionComponent = React.memo(({ componentTemplate }) => { + const { name } = componentTemplate; + + const serializedComponentTemplate = serializeComponentTemplate( + stripEmptyFields(componentTemplate, { + types: ['string', 'object'], + }) as ComponentTemplateDeserialized + ); + + const { + template: { + mappings: serializedMappings, + settings: serializedSettings, + aliases: serializedAliases, + }, + _meta: serializedMeta, + version: serializedVersion, + } = serializedComponentTemplate; + + const SummaryTab = () => ( +
+ + + + + + {/* Version */} + {typeof serializedVersion !== 'undefined' && ( + <> + + + + {serializedVersion} + + )} + + {/* Index settings */} + + + + + {getDescriptionText(serializedSettings)} + + + {/* Mappings */} + + + + + {getDescriptionText(serializedMappings)} + + + {/* Aliases */} + + + + + {getDescriptionText(serializedAliases)} + + + + + + {/* Metadata */} + {serializedMeta && ( + + + + + + + {JSON.stringify(serializedMeta, null, 2)} + + + + )} + + +
+ ); + + const RequestTab = () => { + const endpoint = `PUT _component_template/${name || ''}`; + const templateString = JSON.stringify(serializedComponentTemplate, null, 2); + const request = `${endpoint}\n${templateString}`; + + // Beyond a certain point, highlighting the syntax will bog down performance to unacceptable + // levels. This way we prevent that happening for very large requests. + const language = request.length < 60000 ? 'json' : undefined; + + return ( +
+ + + +

+ +

+
+ + + + + {request} + +
+ ); + }; + + return ( +
+ +

+ +

+
+ + + + , + }, + { + id: 'request', + name: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepReview.requestTabTitle', { + defaultMessage: 'Request', + }), + content: , + }, + ]} + /> +
+ ); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx new file mode 100644 index 00000000000000..10698afc5bc238 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx @@ -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 React from 'react'; + +import { Forms, ComponentTemplateDeserialized } from '../../../shared_imports'; +import { WizardContent } from '../component_template_form'; +import { StepReview } from './step_review'; + +interface Props { + getComponentTemplateData: (wizardContent: WizardContent) => ComponentTemplateDeserialized; +} + +export const StepReviewContainer = React.memo(({ getComponentTemplateData }: Props) => { + const { getData } = Forms.useMultiContentContext(); + + const wizardContent = getData(); + // Build the final template object, providing the wizard content data + const componentTemplate = getComponentTemplateData(wizardContent); + + return ; +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts new file mode 100644 index 00000000000000..59168785b77b2e --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/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. + */ + +export { ComponentTemplateCreate } from './component_template_create'; + +export { ComponentTemplateEdit } from './component_template_edit'; + +export { ComponentTemplateClone } from './component_template_clone'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx index bfea8d39e1203a..ce9e28d0feefe3 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx @@ -7,7 +7,8 @@ import React, { createContext, useContext } from 'react'; import { HttpSetup, DocLinksStart, NotificationsSetup } from 'src/core/public'; -import { getApi, getUseRequest, getSendRequest, getDocumentation } from './lib'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; +import { getApi, getUseRequest, getSendRequest, getDocumentation, getBreadcrumbs } from './lib'; const ComponentTemplatesContext = createContext(undefined); @@ -17,6 +18,7 @@ interface Props { trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; docLinks: DocLinksStart; toasts: NotificationsSetup['toasts']; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; } interface Context { @@ -24,6 +26,7 @@ interface Context { apiBasePath: string; api: ReturnType; documentation: ReturnType; + breadcrumbs: ReturnType; trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; toasts: NotificationsSetup['toasts']; } @@ -35,17 +38,18 @@ export const ComponentTemplatesProvider = ({ value: Props; children: React.ReactNode; }) => { - const { httpClient, apiBasePath, trackMetric, docLinks, toasts } = value; + const { httpClient, apiBasePath, trackMetric, docLinks, toasts, setBreadcrumbs } = value; const useRequest = getUseRequest(httpClient); const sendRequest = getSendRequest(httpClient); const api = getApi(useRequest, sendRequest, apiBasePath, trackMetric); const documentation = getDocumentation(docLinks); + const breadcrumbs = getBreadcrumbs(setBreadcrumbs); return ( {children} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts index e9acfa8dcc56d1..897440feedf705 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts @@ -9,6 +9,8 @@ export const UIM_COMPONENT_TEMPLATE_LIST_LOAD = 'component_template_list_load'; export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete'; export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many'; export const UIM_COMPONENT_TEMPLATE_DETAILS = 'component_template_details'; +export const UIM_COMPONENT_TEMPLATE_CREATE = 'component_template_create'; +export const UIM_COMPONENT_TEMPLATE_UPDATE = 'component_template_update'; // privileges export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates']; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index 52235502e33dfc..7b40435464f2b5 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -10,4 +10,10 @@ export { ComponentTemplateList } from './component_template_list'; export { ComponentTemplateDetailsFlyout } from './component_template_details'; +export { + ComponentTemplateCreate, + ComponentTemplateEdit, + ComponentTemplateClone, +} from './component_template_wizard'; + export * from './component_template_selector'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 63fe127c6b2d7f..87f6767f14d5c3 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -4,8 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ComponentTemplateListItem, ComponentTemplateDeserialized, Error } from '../shared_imports'; -import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE } from '../constants'; +import { + ComponentTemplateListItem, + ComponentTemplateDeserialized, + ComponentTemplateSerialized, + Error, +} from '../shared_imports'; +import { + UIM_COMPONENT_TEMPLATE_DELETE_MANY, + UIM_COMPONENT_TEMPLATE_DELETE, + UIM_COMPONENT_TEMPLATE_CREATE, + UIM_COMPONENT_TEMPLATE_UPDATE, +} from '../constants'; import { UseRequestHook, SendRequestHook } from './request'; export const getApi = ( @@ -44,9 +54,36 @@ export const getApi = ( }); } + async function createComponentTemplate(componentTemplate: ComponentTemplateSerialized) { + const result = await sendRequest({ + path: `${apiBasePath}/component_templates`, + method: 'post', + body: JSON.stringify(componentTemplate), + }); + + trackMetric('count', UIM_COMPONENT_TEMPLATE_CREATE); + + return result; + } + + async function updateComponentTemplate(componentTemplate: ComponentTemplateDeserialized) { + const { name } = componentTemplate; + const result = await sendRequest({ + path: `${apiBasePath}/component_templates/${encodeURIComponent(name)}`, + method: 'put', + body: JSON.stringify(componentTemplate), + }); + + trackMetric('count', UIM_COMPONENT_TEMPLATE_UPDATE); + + return result; + } + return { useLoadComponentTemplates, deleteComponentTemplates, useLoadComponentTemplate, + createComponentTemplate, + updateComponentTemplate, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts new file mode 100644 index 00000000000000..033df5a9562ed7 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts @@ -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 { i18n } from '@kbn/i18n'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; + +export const getBreadcrumbs = (setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']) => { + const baseBreadcrumbs = [ + { + text: i18n.translate('xpack.idxMgmt.componentTemplate.breadcrumb.homeLabel', { + defaultMessage: 'Index Management', + }), + href: '/', + }, + { + text: i18n.translate('xpack.idxMgmt.componentTemplate.breadcrumb.componentTemplatesLabel', { + defaultMessage: 'Component templates', + }), + href: '/component_templates', + }, + ]; + + const setCreateBreadcrumbs = () => { + const createBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate( + 'xpack.idxMgmt.componentTemplate.breadcrumb.createComponentTemplateLabel', + { + defaultMessage: 'Create component template', + } + ), + }, + ]; + + return setBreadcrumbs(createBreadcrumbs); + }; + + const setEditBreadcrumbs = () => { + const editBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate( + 'xpack.idxMgmt.componentTemplate.breadcrumb.editComponentTemplateLabel', + { + defaultMessage: 'Edit component template', + } + ), + }, + ]; + + return setBreadcrumbs(editBreadcrumbs); + }; + + return { + setCreateBreadcrumbs, + setEditBreadcrumbs, + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts index 9d20ae9d2ec76a..db06877d6e81a0 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts @@ -11,6 +11,8 @@ export const getDocumentation = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocL const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; return { + esDocsBase, componentTemplates: `${esDocsBase}/indices-component-template.html`, + componentTemplatesMetadata: `${esDocsBase}/indices-component-template.html#component-templates-metadata`, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts index 9a91312f832948..29273bd946e10e 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts @@ -9,3 +9,7 @@ export * from './api'; export * from './request'; export * from './documentation'; + +export * from './breadcrumbs'; + +export { attemptToDecodeURI } from './utils'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts new file mode 100644 index 00000000000000..48a6d843c4fa7f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts @@ -0,0 +1,18 @@ +/* + * 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 attemptToDecodeURI = (value: string) => { + let result: string; + + try { + result = decodeURI(value); + result = decodeURIComponent(result); + } catch (e) { + result = decodeURIComponent(value); + } + + return result; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index bd19c2004894ce..80e222f4f77064 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -21,10 +21,44 @@ export { Forms, } from '../../../../../../../src/plugins/es_ui_shared/public'; -export { TabMappings, TabSettings, TabAliases } from '../shared'; +export { + serializers, + fieldValidators, + fieldFormatters, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { + FormSchema, + FIELD_TYPES, + VALIDATION_TYPES, + FieldConfig, + useForm, + Form, + getUseField, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { + getFormRow, + Field, + JsonEditorField, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { isJSON } from '../../../../../../../src/plugins/es_ui_shared/static/validators/string'; + +export { + TabMappings, + TabSettings, + TabAliases, + CommonWizardSteps, + StepSettingsContainer, + StepMappingsContainer, + StepAliasesContainer, +} from '../shared'; export { ComponentTemplateSerialized, ComponentTemplateDeserialized, ComponentTemplateListItem, } from '../../../../common'; + +export { serializeComponentTemplate } from '../../../../common/lib'; diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index ff54b4b1bfe350..7b053a15b26d02 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -27,7 +27,7 @@ export const renderApp = ( const { i18n, docLinks, notifications } = core; const { Context: I18nContext } = i18n; - const { services, history } = dependencies; + const { services, history, setBreadcrumbs } = dependencies; const componentTemplateProviderValues = { httpClient: services.httpService.httpClient, @@ -35,6 +35,7 @@ export const renderApp = ( trackMetric: services.uiMetricService.trackMetric.bind(services.uiMetricService), docLinks, toasts: notifications.toasts, + setBreadcrumbs, }; render( diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 258f32865720af..6145ea410b0e8d 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -50,6 +50,7 @@ export async function mountManagementSection( }, services, history, + setBreadcrumbs, }; return renderApp(element, { core, dependencies: appDependencies }); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts index 175254ca16e3d0..56ee9640d3d077 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts @@ -4,17 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; +import { serializeComponentTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { componentTemplateSchema } from './schema_validation'; -const bodySchema = schema.object({ - name: schema.string(), - ...componentTemplateSchema, -}); - export const registerCreateRoute = ({ router, license, @@ -24,13 +19,15 @@ export const registerCreateRoute = ({ { path: addBasePath('/component_templates'), validate: { - body: bodySchema, + body: componentTemplateSchema, }, }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; - const { name, ...componentTemplateDefinition } = req.body; + const serializedComponentTemplate = serializeComponentTemplate(req.body); + + const { name } = req.body; try { // Check that a component template with the same name doesn't already exist @@ -60,7 +57,7 @@ export const registerCreateRoute = ({ try { const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { name, - body: componentTemplateDefinition, + body: serializedComponentTemplate, }); return res.ok({ body: response }); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts index 7d32637c6b9779..a1fc2581272294 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts @@ -5,7 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -export const componentTemplateSchema = { +export const componentTemplateSchema = schema.object({ + name: schema.string(), template: schema.object({ settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })), @@ -13,4 +14,7 @@ export const componentTemplateSchema = { }), version: schema.maybe(schema.number()), _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), -}; + _kbnMeta: schema.object({ + usedBy: schema.arrayOf(schema.string()), + }), +}); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts index 7e447bb110c67b..47834a2cf499d3 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -9,8 +9,6 @@ import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { componentTemplateSchema } from './schema_validation'; -const bodySchema = schema.object(componentTemplateSchema); - const paramsSchema = schema.object({ name: schema.string(), }); @@ -24,7 +22,7 @@ export const registerUpdateRoute = ({ { path: addBasePath('/component_templates/{name}'), validate: { - body: bodySchema, + body: componentTemplateSchema, params: paramsSchema, }, }, diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts index 56b4ec45b42b71..1a00eaba35aa15 100644 --- a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -146,6 +146,9 @@ export default function ({ getService }: FtrProviderContext) { id: 10, }, }, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -162,6 +165,9 @@ export default function ({ getService }: FtrProviderContext) { .send({ name: REQUIRED_FIELDS_COMPONENT_NAME, template: {}, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -177,6 +183,9 @@ export default function ({ getService }: FtrProviderContext) { .send({ name: COMPONENT_NAME, template: {}, + _kbnMeta: { + usedBy: [], + }, }) .expect(409); @@ -233,7 +242,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ ...COMPONENT, + name: COMPONENT_NAME, version: 1, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -250,7 +263,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ ...COMPONENT, + name: 'component_does_not_exist', version: 1, + _kbnMeta: { + usedBy: [], + }, }) .expect(404); From a9b543d9bca049bda4fe03977f23de9561765873 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jul 2020 18:40:12 -0400 Subject: [PATCH 18/57] reenable regression and classification functional tests (#70661) --- .../apps/ml/data_frame_analytics/classification_creation.ts | 4 ++-- .../apps/ml/data_frame_analytics/regression_creation.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 4a79610cadbde6..5c750e119063e2 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // flaky test https://github.com/elastic/kibana/issues/70455 - describe.skip('classification creation', function () { + + describe('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 33f0ee9cd99ac7..9ddf2dfc482694 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // flaky test https://github.com/elastic/kibana/issues/70455 - describe.skip('regression creation', function () { + + describe('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); From 59924243127bb314acb5d921bff882079ce926e2 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Mon, 6 Jul 2020 18:52:00 -0400 Subject: [PATCH 19/57] add button link to ingest (#70142) update security solution empty page --- .../sections/epm/screens/home/index.tsx | 15 ++++- .../detection_engine/detection_engine.tsx | 4 +- .../detection_engine_empty_page.test.tsx | 19 ------ .../detection_engine_empty_page.tsx | 28 -------- .../detection_engine/rules/details/index.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../common/components/empty_page/index.tsx | 15 +++-- .../common/hooks/endpoint/ingest_enabled.ts | 34 ++++++++++ .../public/common/translations.ts | 14 ++-- .../public/hosts/pages/details/index.tsx | 4 +- .../public/hosts/pages/hosts.tsx | 4 +- .../public/hosts/pages/hosts_empty_page.tsx | 34 ---------- .../pages/endpoint_hosts/view/hooks.ts | 6 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../public/network/pages/ip_details/index.tsx | 4 +- .../public/network/pages/network.tsx | 4 +- .../network/pages/network_empty_page.tsx | 34 ---------- .../components/overview_empty/index.tsx | 54 +++++++++++++-- .../public/overview/pages/overview.test.tsx | 66 ++++++++++++++----- .../source_status/elasticsearch_adapter.ts | 3 + 20 files changed, 180 insertions(+), 170 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx create mode 100644 x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts delete mode 100644 x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx delete mode 100644 x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index e00b63e29019e1..c68833c1b2d951 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -5,7 +5,7 @@ */ import React, { useState } from 'react'; -import { useRouteMatch, Switch, Route } from 'react-router-dom'; +import { useRouteMatch, Switch, Route, useLocation, useHistory } from 'react-router-dom'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { i18n } from '@kbn/i18n'; import { PAGE_ROUTING_PATHS } from '../../../../constants'; @@ -114,7 +114,10 @@ function InstalledPackages() { function AvailablePackages() { useBreadcrumbs('integrations_all'); - const [selectedCategory, setSelectedCategory] = useState(''); + const history = useHistory(); + const queryParams = new URLSearchParams(useLocation().search); + const initialCategory = queryParams.get('category') || ''; + const [selectedCategory, setSelectedCategory] = useState(initialCategory); const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({ category: selectedCategory, }); @@ -141,7 +144,13 @@ function AvailablePackages() { isLoading={isLoadingCategories} categories={categories} selectedCategory={selectedCategory} - onCategoryChange={({ id }: CategorySummaryItem) => setSelectedCategory(id)} + onCategoryChange={({ id }: CategorySummaryItem) => { + // clear category query param in the url + if (queryParams.get('category')) { + history.push({}); + } + setSelectedCategory(id); + }} /> ) : null; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx index 5c525a85534775..b39d51e2de95fb 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx @@ -31,7 +31,7 @@ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; import { useUserInfo } from '../../components/user_info'; -import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; +import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; @@ -159,7 +159,7 @@ export const DetectionEnginePageComponent: React.FC = ({ ) : ( - + )} diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx deleted file mode 100644 index 039c878b121a09..00000000000000 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx +++ /dev/null @@ -1,19 +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 { shallow } from 'enzyme'; - -import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; -jest.mock('../../../common/lib/kibana'); - -describe('DetectionEngineEmptyPage', () => { - it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('EmptyPage')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx deleted file mode 100644 index 0c58f5620964ba..00000000000000 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx +++ /dev/null @@ -1,28 +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 { useKibana } from '../../../common/lib/kibana'; -import { EmptyPage } from '../../../common/components/empty_page'; -import * as i18n from '../../../common/translations'; -import { ADD_DATA_PATH } from '../../../../common/constants'; - -export const DetectionEngineEmptyPage = React.memo(() => ( - -)); -DetectionEngineEmptyPage.displayName = 'DetectionEngineEmptyPage'; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx index b937e95c0a57e4..c73613842a8724 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -43,7 +43,7 @@ import { DetectionEngineHeaderPage } from '../../../../components/detection_engi import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; import { AlertsTable } from '../../../../components/alerts_table'; import { useUserInfo } from '../../../../components/user_info'; -import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; +import { OverviewEmpty } from '../../../../../overview/components/overview_empty'; import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; @@ -426,7 +426,7 @@ export const RuleDetailsPageComponent: FC = ({ - + )} diff --git a/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap index 65893f84f5e563..623b15aa76d127 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap @@ -18,7 +18,7 @@ exports[`renders correctly 1`] = ` } - iconType="securityAnalyticsApp" + iconType="logoSecurity" title={

My Super Title diff --git a/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx index a067c1d28f87fa..f6d6752729b6d9 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, IconType } from '@elastic/eui'; -import React from 'react'; +import React, { MouseEventHandler, ReactNode } from 'react'; import styled from 'styled-components'; const EmptyPrompt = styled(EuiEmptyPrompt)` @@ -19,12 +19,14 @@ interface EmptyPageProps { actionPrimaryLabel: string; actionPrimaryTarget?: string; actionPrimaryUrl: string; + actionPrimaryFill?: boolean; actionSecondaryIcon?: IconType; actionSecondaryLabel?: string; actionSecondaryTarget?: string; actionSecondaryUrl?: string; + actionSecondaryOnClick?: MouseEventHandler; 'data-test-subj'?: string; - message?: string; + message?: ReactNode; title: string; } @@ -34,23 +36,25 @@ export const EmptyPage = React.memo( actionPrimaryLabel, actionPrimaryTarget, actionPrimaryUrl, + actionPrimaryFill = true, actionSecondaryIcon, actionSecondaryLabel, actionSecondaryTarget, actionSecondaryUrl, + actionSecondaryOnClick, message, title, ...rest }) => ( {title}

} body={message &&

{message}

} actions={ ( {actionSecondaryLabel && actionSecondaryUrl && ( + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} {actionSecondaryLabel} diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts new file mode 100644 index 00000000000000..c201d85a270c02 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.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 { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +/** + * Returns an object which ingest permissions are allowed + */ +export const useIngestEnabledCheck = (): { + allEnabled: boolean; + show: boolean; + write: boolean; + read: boolean; +} => { + const { services } = useKibana(); + + // Check if Ingest Manager is present in the configuration + const show = services.application.capabilities.ingestManager?.show ?? false; + const write = services.application.capabilities.ingestManager?.write ?? false; + const read = services.application.capabilities.ingestManager?.read ?? false; + + // Check if all Ingest Manager permissions are enabled + const allEnabled = show && read && write ? true : false; + + return { + allEnabled, + show, + write, + read, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/translations.ts b/x-pack/plugins/security_solution/public/common/translations.ts index 677543ec0dba68..413119fb40f141 100644 --- a/x-pack/plugins/security_solution/public/common/translations.ts +++ b/x-pack/plugins/security_solution/public/common/translations.ts @@ -10,11 +10,6 @@ export const EMPTY_TITLE = i18n.translate('xpack.securitySolution.pages.common.e defaultMessage: 'Welcome to Security Solution. Let’s get you started.', }); -export const EMPTY_MESSAGE = i18n.translate('xpack.securitySolution.pages.common.emptyMessage', { - defaultMessage: - 'To begin using security information and event management (Security Solution), you’ll need to add security solution related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', -}); - export const EMPTY_ACTION_PRIMARY = i18n.translate( 'xpack.securitySolution.pages.common.emptyActionPrimary', { @@ -25,6 +20,13 @@ export const EMPTY_ACTION_PRIMARY = i18n.translate( export const EMPTY_ACTION_SECONDARY = i18n.translate( 'xpack.securitySolution.pages.common.emptyActionSecondary', { - defaultMessage: 'View getting started guide', + defaultMessage: 'getting started guide.', + } +); + +export const EMPTY_ACTION_ENDPOINT = i18n.translate( + 'xpack.securitySolution.pages.common.emptyActionEndpoint', + { + defaultMessage: 'Add data with Elastic Agent (Beta)', } ); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 46823f037b61cf..bb0317f0482b03 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -38,7 +38,7 @@ import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '. import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; -import { HostsEmptyPage } from '../hosts_empty_page'; +import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { HostDetailsTabs } from './details_tabs'; import { navTabsHostDetails } from './nav_tabs'; import { HostDetailsProps } from './types'; @@ -194,7 +194,7 @@ const HostDetailsComponent = React.memo( - + )} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 90438aec7c27e4..a2f83bf0965f36 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -33,7 +33,7 @@ import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from import { SpyRoute } from '../../common/utils/route/spy_routes'; import { esQuery } from '../../../../../../src/plugins/data/public'; import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; -import { HostsEmptyPage } from './hosts_empty_page'; +import { OverviewEmpty } from '../../overview/components/overview_empty'; import { HostsTabs } from './hosts_tabs'; import { navTabsHosts } from './nav_tabs'; import * as i18n from './translations'; @@ -141,7 +141,7 @@ export const HostsComponent = React.memo( - + )} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx deleted file mode 100644 index a01e249561e5c5..00000000000000 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx +++ /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 React from 'react'; - -import { EmptyPage } from '../../common/components/empty_page'; -import { useKibana } from '../../common/lib/kibana'; -import * as i18n from '../../common/translations'; -import { ADD_DATA_PATH } from '../../../common/constants'; - -export const HostsEmptyPage = React.memo(() => { - const { http, docLinks } = useKibana().services; - const basePath = http.basePath.get(); - - return ( - - ); -}); - -HostsEmptyPage.displayName = 'HostsEmptyPage'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index 68198b691da403..b048a8f69b5d23 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -24,16 +24,16 @@ export function useHostSelector(selector: (state: HostState) => TSele /** * Returns an object that contains Ingest app and URL information */ -export const useHostIngestUrl = (): { url: string; appId: string; appPath: string } => { +export const useIngestUrl = (subpath: string): { url: string; appId: string; appPath: string } => { const { services } = useKibana(); return useMemo(() => { - const appPath = `#/fleet`; + const appPath = `#/${subpath}`; return { url: `${services.application.getUrlForApp('ingestManager')}${appPath}`, appId: 'ingestManager', appPath, }; - }, [services.application]); + }, [services.application, subpath]); }; /** diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap index d7af8d6910f45c..93dafeff34ce9f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap @@ -9,7 +9,7 @@ exports[`Ip Details it matches the snapshot 1`] = ` border={true} title="123.456.78.90" /> - + - + )} diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index bdaac1ac049e5e..5767951f9f6b31 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -34,7 +34,7 @@ import { SpyRoute } from '../../common/utils/route/spy_routes'; import { networkModel } from '../store'; import { navTabsNetwork, NetworkRoutes, NetworkRoutesLoading } from './navigation'; import { filterNetworkData } from './navigation/alerts_query_tab_body'; -import { NetworkEmptyPage } from './network_empty_page'; +import { OverviewEmpty } from '../../overview/components/overview_empty'; import * as i18n from './translations'; import { NetworkComponentProps } from './types'; import { NetworkRouteType } from './navigation/types'; @@ -164,7 +164,7 @@ const NetworkComponent = React.memo( ) : ( - + )} diff --git a/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx b/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx deleted file mode 100644 index dce3f85797f121..00000000000000 --- a/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx +++ /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 React from 'react'; - -import { useKibana } from '../../common/lib/kibana'; -import { EmptyPage } from '../../common/components/empty_page'; -import * as i18n from '../../common/translations'; -import { ADD_DATA_PATH } from '../../../common/constants'; - -export const NetworkEmptyPage = React.memo(() => { - const { http, docLinks } = useKibana().services; - const basePath = http.basePath.get(); - - return ( - - ); -}); - -NetworkEmptyPage.displayName = 'NetworkEmptyPage'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index 00db437bce11ed..33413be10079e5 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -5,27 +5,67 @@ */ import React from 'react'; - +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink } from '@elastic/eui'; import * as i18nCommon from '../../../common/translations'; import { EmptyPage } from '../../../common/components/empty_page'; import { useKibana } from '../../../common/lib/kibana'; import { ADD_DATA_PATH } from '../../../../common/constants'; +import { useIngestUrl } from '../../../management/pages/endpoint_hosts/view/hooks'; +import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled'; const OverviewEmptyComponent: React.FC = () => { const { http, docLinks } = useKibana().services; const basePath = http.basePath.get(); + const { appId: ingestAppId, appPath: ingestPath, url: ingestUrl } = useIngestUrl( + 'integrations?category=security' + ); + const handleOnClick = useNavigateToAppEventHandler(ingestAppId, { path: ingestPath }); + const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); - return ( + return isIngestEnabled === true ? ( + + + + {i18nCommon.EMPTY_ACTION_SECONDARY} + + + } + title={i18nCommon.EMPTY_TITLE} + /> + ) : ( + + + {i18nCommon.EMPTY_ACTION_SECONDARY} + + + } title={i18nCommon.EMPTY_TITLE} /> ); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 9613a1e7210a3b..6f13f64ca1bffa 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -16,6 +16,7 @@ import { UseMessagesStorage, } from '../../common/containers/local_storage/use_messages_storage'; import { Overview } from './index'; +import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); @@ -33,6 +34,7 @@ jest.mock('../../common/components/search_bar', () => ({ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../common/hooks/endpoint/ingest_enabled'); jest.mock('../../common/containers/local_storage/use_messages_storage'); const endpointNoticeMessage = (hasMessageValue: boolean) => { @@ -47,26 +49,54 @@ const endpointNoticeMessage = (hasMessageValue: boolean) => { describe('Overview', () => { describe('rendering', () => { - test('it renders the Setup Instructions text when no index is available', async () => { - (useWithSource as jest.Mock).mockReturnValue({ - indicesExist: false, + describe('when no index is available', () => { + beforeEach(() => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + }); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock< + UseMessagesStorage + >; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); }); - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + it('renders the Setup Instructions text', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + }); - const wrapper = mount( - - - - - - ); + it('does not show Endpoint get ready button when ingest is not enabled', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page-secondary-action"]').exists()).toBe(false); + }); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + it('shows Endpoint get ready button when ingest is enabled', () => { + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page-secondary-action"]').exists()).toBe(true); + }); }); - test('it DOES NOT render the Getting started text when an index is available', async () => { + it('it DOES NOT render the Getting started text when an index is available', () => { (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, @@ -85,7 +115,7 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); - test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', async () => { + test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', () => { (useWithSource as jest.Mock).mockReturnValueOnce({ indicesExist: true, indexPattern: {}, @@ -109,7 +139,7 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(true); }); - test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', async () => { + test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', () => { (useWithSource as jest.Mock).mockReturnValueOnce({ indicesExist: true, indexPattern: {}, @@ -133,7 +163,7 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); }); - test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', async () => { + test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', () => { (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, @@ -152,7 +182,7 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); }); - test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', async () => { + test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', () => { (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, diff --git a/x-pack/plugins/security_solution/server/lib/source_status/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/source_status/elasticsearch_adapter.ts index 8872d347da8267..ab491f54854e44 100644 --- a/x-pack/plugins/security_solution/server/lib/source_status/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/source_status/elasticsearch_adapter.ts @@ -8,6 +8,7 @@ import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { SourceStatusAdapter } from './index'; import { buildQuery } from './query.dsl'; import { ApmServiceNameAgg } from './types'; +import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; const APM_INDEX_NAME = 'apm-*-transaction*'; @@ -18,6 +19,8 @@ export class ElasticsearchSourceStatusAdapter implements SourceStatusAdapter { // Intended flow to determine app-empty state is to first check siem indices (as this is a quick shard count), and // if no shards exist only then perform the heavier APM query. This optimizes for normal use when siem data exists try { + // Add endpoint metadata index to indices to check + indexNames.push(ENDPOINT_METADATA_INDEX); // Remove APM index if exists, and only query if length > 0 in case it's the only index provided const nonApmIndexNames = indexNames.filter((name) => name !== APM_INDEX_NAME); const indexCheckResponse = await (nonApmIndexNames.length > 0 From 57915e164169df6026e766bcfe6a754cfa6228a2 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 6 Jul 2020 16:38:45 -0700 Subject: [PATCH 20/57] ServiceNow push to Incident generic implementation (supporting both Case specific and generic Alerts) (#68464) * Draft ServiceNow generic implementation * simple working servicenow incident per alert * fixed running times * rely on externalId for update incident on the next execution * Added consumer to the action type to be able to split ServiceNow for Cases and Alerts * Added subActions support for ServiceNow action form * Basic version for Alerts part for ServiceNow * Keep Case ServiceNow functionality working * Revert changes on app_router * Fixed type checks * Fixed language check issues * Fixed actions unit tests * Fixed functional tests * Fixed jest tests * fixed tests * Copied case mappings to alerting plugin * made consumer optional * Cleanup tests * more cleanup * Fixed jest tests and type checks * fixed tests * fixed servicenow validation tests * Added ServiceNow unit tests * Removed consumer for actions * fixed client side isCaseOwned support * fixed failing tests * fixed jest tests * Fixed URL validation * fixed due to comments * fixed tests * fixed jest tests * Fixed due to comments. Moved ServiceNow filtering in case plugin to server side * fixed mock for ServiceNow * fixed consumer config * fixed test * fixed type check * Fixed jest test * fixed type check --- .../pre-configured-connectors.asciidoc | 4 +- .../plugins/actions/server/actions_client.ts | 2 +- .../server/builtin_action_types/case/api.ts | 2 +- .../builtin_action_types/case/schema.ts | 2 +- .../server/builtin_action_types/case/types.ts | 2 +- .../builtin_action_types/case/utils.test.ts | 139 +--------- .../server/builtin_action_types/case/utils.ts | 54 +--- .../server/builtin_action_types/index.ts | 2 +- .../server/builtin_action_types/jira/mocks.ts | 2 +- .../builtin_action_types/jira/service.test.ts | 6 +- .../builtin_action_types/jira/service.ts | 2 +- .../lib/axios_utils.test.ts | 105 +++++++ .../builtin_action_types/lib/axios_utils.ts | 60 ++++ .../servicenow/api.test.ts | 257 +++++++++++------ .../builtin_action_types/servicenow/api.ts | 142 +++++++++- .../servicenow/case_shema.ts | 36 +++ .../servicenow/case_types.ts | 64 +++++ .../builtin_action_types/servicenow/index.ts | 107 +++++-- .../builtin_action_types/servicenow/mocks.ts | 31 +-- .../builtin_action_types/servicenow/schema.ts | 70 +++++ .../servicenow/service.test.ts | 56 +--- .../servicenow/service.ts | 62 ++--- .../servicenow/translations.ts | 18 +- .../builtin_action_types/servicenow/types.ts | 97 ++++++- .../servicenow/validators.ts | 34 ++- x-pack/plugins/case/common/api/cases/case.ts | 2 +- x-pack/plugins/case/common/constants.ts | 1 + .../routes/api/__mocks__/request_responses.ts | 3 +- .../api/cases/configure/get_connectors.ts | 9 +- .../components/configure_cases/index.test.tsx | 10 +- .../components/configure_cases/index.tsx | 1 + .../public/cases/containers/configure/mock.ts | 6 +- .../public/cases/containers/mock.ts | 2 +- .../use_post_push_to_service.test.tsx | 2 +- .../containers/use_post_push_to_service.tsx | 2 +- .../public/common/lib/connectors/config.ts | 5 +- .../public/common/lib/connectors/index.ts | 1 - .../lib/connectors/servicenow/flyout.tsx | 87 ------ .../lib/connectors/servicenow/index.tsx | 47 ---- .../lib/connectors/servicenow/translations.ts | 30 -- .../common/lib/connectors/servicenow/types.ts | 22 -- .../security_solution/public/plugin.tsx | 3 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - x-pack/plugins/triggers_actions_ui/README.md | 7 +- .../components/builtin_action_types/index.ts | 2 + .../case_mappings/field_mapping.tsx | 141 ++++++++++ .../case_mappings/field_mapping_row.tsx | 78 ++++++ .../servicenow/case_mappings/translations.ts | 190 +++++++++++++ .../servicenow/case_mappings/types.ts} | 16 +- .../servicenow/case_mappings/utils.ts | 38 +++ .../servicenow/config.ts | 3 +- .../builtin_action_types/servicenow/index.ts | 7 + .../builtin_action_types}/servicenow/logo.svg | 0 .../servicenow/servicenow.test.tsx | 97 +++++++ .../servicenow/servicenow.tsx | 67 +++++ .../servicenow/servicenow_connectors.test.tsx | 83 ++++++ .../servicenow/servicenow_connectors.tsx | 182 ++++++++++++ .../servicenow/servicenow_params.test.tsx | 43 +++ .../servicenow/servicenow_params.tsx | 262 ++++++++++++++++++ .../servicenow/translations.ts | 133 +++++++++ .../builtin_action_types/servicenow/types.ts | 46 +++ .../slack/slack_connectors.tsx | 2 +- .../context/actions_connectors_context.tsx | 1 + .../application/lib/value_validators.test.ts | 16 +- .../application/lib/value_validators.ts | 12 + .../action_connector_form.tsx | 3 + .../action_form.test.tsx | 10 + .../action_connector_form/action_form.tsx | 10 +- .../connector_add_flyout.tsx | 2 + .../connector_add_modal.tsx | 3 + .../connector_edit_flyout.tsx | 2 + .../actions_connectors_list.test.tsx | 130 +++++---- .../components/actions_connectors_list.tsx | 10 +- .../public/common/index.ts | 2 + .../triggers_actions_ui/public/types.ts | 1 + .../builtin_action_types/servicenow.ts | 21 +- .../server/servicenow_simulation.ts | 4 + .../actions/builtin_action_types/jira.ts | 18 +- .../builtin_action_types/servicenow.ts | 123 +++----- 80 files changed, 2559 insertions(+), 797 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/flyout.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts rename x-pack/plugins/{actions/server/builtin_action_types/servicenow/config.ts => triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts} (50%) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts rename x-pack/plugins/{security_solution/public/common/lib/connectors => triggers_actions_ui/public/application/components/builtin_action_types}/servicenow/config.ts (91%) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/index.ts rename x-pack/plugins/{security_solution/public/common/lib/connectors => triggers_actions_ui/public/application/components/builtin_action_types}/servicenow/logo.svg (100%) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc index b1cf2d650e576f..e3f1703f08e88e 100644 --- a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc @@ -28,12 +28,12 @@ two out-of-the box connectors: <> and < actionTypeId: .slack <2> name: 'Slack #xyz' <3> - secrets: <4> + secrets: webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' webhook-service: actionTypeId: .webhook name: 'Email service' - config: + config: <4> url: 'https://email-alert-service.elastic.co' method: post headers: diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index a512d314fb7e25..44f9cfd5c9e618 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -148,7 +148,7 @@ export class ActionsClient { this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - const result = await this.savedObjectsClient.update('action', id, { + const result = await this.savedObjectsClient.update('action', id, { actionTypeId, name, config: validatedActionTypeConfig as SavedObjectAttributes, diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts index 6dc8a9cc9af6ad..de4b7edaed3da0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts @@ -41,7 +41,7 @@ const pushToServiceHandler = async ({ } const fields = prepareFieldsForTransformation({ - params, + externalCase: params.externalCase, mapping, defaultPipes, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts index 33b2ad6d186843..f47686c911ff09 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts @@ -67,7 +67,7 @@ export const ExecutorSubActionSchema = schema.oneOf([ ]); export const ExecutorSubActionPushParamsSchema = schema.object({ - caseId: schema.string(), + savedObjectId: schema.string(), title: schema.string(), description: schema.nullable(schema.string()), comments: schema.nullable(schema.arrayOf(CommentSchema)), diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts index 992b2cb16fb06a..de96864d0b2959 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -144,7 +144,7 @@ export interface PipedField { } export interface PrepareFieldsForTransformArgs { - params: PushToServiceApiParams; + externalCase: Record; mapping: Map; defaultPipes?: string[]; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts index 017fc73efae204..dbb18fa5c695c4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; - import { normalizeMapping, buildMap, @@ -13,19 +11,11 @@ import { prepareFieldsForTransformation, transformFields, transformComments, - addTimeZoneToDate, - throwIfNotAlive, - request, - patch, - getErrorMessage, } from './utils'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; import { Comment, MapRecord, PushToServiceApiParams } from './types'; -jest.mock('axios'); -const axiosMock = (axios as unknown) as jest.Mock; - const mapping: MapRecord[] = [ { source: 'title', target: 'short_description', actionType: 'overwrite' }, { source: 'description', target: 'description', actionType: 'append' }, @@ -63,7 +53,7 @@ const maliciousMapping: MapRecord[] = [ ]; const fullParams: PushToServiceApiParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', title: 'a title', description: 'a description', createdAt: '2020-03-13T08:34:53.450Z', @@ -132,7 +122,7 @@ describe('buildMap', () => { describe('mapParams', () => { test('maps params correctly', () => { const params = { - caseId: '123', + savedObjectId: '123', incidentId: '456', title: 'Incident title', description: 'Incident description', @@ -148,7 +138,7 @@ describe('mapParams', () => { test('do not add fields not in mapping', () => { const params = { - caseId: '123', + savedObjectId: '123', incidentId: '456', title: 'Incident title', description: 'Incident description', @@ -164,7 +154,7 @@ describe('mapParams', () => { describe('prepareFieldsForTransformation', () => { test('prepare fields with defaults', () => { const res = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, }); expect(res).toEqual([ @@ -185,7 +175,7 @@ describe('prepareFieldsForTransformation', () => { test('prepare fields with default pipes', () => { const res = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, defaultPipes: ['myTestPipe'], }); @@ -209,7 +199,7 @@ describe('prepareFieldsForTransformation', () => { describe('transformFields', () => { test('transform fields for creation correctly', () => { const fields = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, }); @@ -226,14 +216,7 @@ describe('transformFields', () => { test('transform fields for update correctly', () => { const fields = prepareFieldsForTransformation({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { - username: 'anotherUser', - fullName: 'Another User', - }, - }, + externalCase: fullParams.externalCase, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); @@ -262,7 +245,7 @@ describe('transformFields', () => { test('add newline character to descripton', () => { const fields = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); @@ -280,7 +263,7 @@ describe('transformFields', () => { test('append username if fullname is undefined when create', () => { const fields = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, }); @@ -300,14 +283,7 @@ describe('transformFields', () => { test('append username if fullname is undefined when update', () => { const fields = prepareFieldsForTransformation({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { - username: 'anotherUser', - fullName: 'Another User', - }, - }, + externalCase: fullParams.externalCase, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); @@ -479,98 +455,3 @@ describe('transformComments', () => { ]); }); }); - -describe('addTimeZoneToDate', () => { - test('adds timezone with default', () => { - const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z'); - expect(date).toBe('2020-04-14T15:01:55.456Z GMT'); - }); - - test('adds timezone correctly', () => { - const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z', 'PST'); - expect(date).toBe('2020-04-14T15:01:55.456Z PST'); - }); -}); - -describe('throwIfNotAlive ', () => { - test('throws correctly when status is invalid', async () => { - expect(() => { - throwIfNotAlive(404, 'application/json'); - }).toThrow('Instance is not alive.'); - }); - - test('throws correctly when content is invalid', () => { - expect(() => { - throwIfNotAlive(200, 'application/html'); - }).toThrow('Instance is not alive.'); - }); - - test('do NOT throws with custom validStatusCodes', async () => { - expect(() => { - throwIfNotAlive(404, 'application/json', [404]); - }).not.toThrow('Instance is not alive.'); - }); -}); - -describe('request', () => { - beforeEach(() => { - axiosMock.mockImplementation(() => ({ - status: 200, - headers: { 'content-type': 'application/json' }, - data: { incidentId: '123' }, - })); - }); - - test('it fetch correctly with defaults', async () => { - const res = await request({ axios, url: '/test' }); - - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); - expect(res).toEqual({ - status: 200, - headers: { 'content-type': 'application/json' }, - data: { incidentId: '123' }, - }); - }); - - test('it fetch correctly', async () => { - const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); - - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); - expect(res).toEqual({ - status: 200, - headers: { 'content-type': 'application/json' }, - data: { incidentId: '123' }, - }); - }); - - test('it throws correctly', async () => { - axiosMock.mockImplementation(() => ({ - status: 404, - headers: { 'content-type': 'application/json' }, - data: { incidentId: '123' }, - })); - - await expect(request({ axios, url: '/test' })).rejects.toThrow(); - }); -}); - -describe('patch', () => { - beforeEach(() => { - axiosMock.mockImplementation(() => ({ - status: 200, - headers: { 'content-type': 'application/json' }, - })); - }); - - test('it fetch correctly', async () => { - await patch({ axios, url: '/test', data: { id: '123' } }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); - }); -}); - -describe('getErrorMessage', () => { - test('it returns the correct error message', () => { - const msg = getErrorMessage('My connector name', 'An error has occurred'); - expect(msg).toBe('[Action][My connector name]: An error has occurred'); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 2d81c2bf4e15fb..676a4776d00552 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -6,7 +6,6 @@ import { curry, flow, get } from 'lodash'; import { schema } from '@kbn/config-schema'; -import { AxiosInstance, Method, AxiosResponse } from 'axios'; import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; @@ -134,65 +133,18 @@ export const createConnector = ({ }); }; -export const throwIfNotAlive = ( - status: number, - contentType: string, - validStatusCodes: number[] = [200, 201, 204] -) => { - if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { - throw new Error('Instance is not alive.'); - } -}; - -export const request = async ({ - axios, - url, - method = 'get', - data, -}: { - axios: AxiosInstance; - url: string; - method?: Method; - data?: T; -}): Promise => { - const res = await axios(url, { method, data: data ?? {} }); - throwIfNotAlive(res.status, res.headers['content-type']); - return res; -}; - -export const patch = async ({ - axios, - url, - data, -}: { - axios: AxiosInstance; - url: string; - data: T; -}): Promise => { - return request({ - axios, - url, - method: 'patch', - data, - }); -}; - -export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { - return `${date} ${timezone}`; -}; - export const prepareFieldsForTransformation = ({ - params, + externalCase, mapping, defaultPipes = ['informationCreated'], }: PrepareFieldsForTransformArgs): PipedField[] => { - return Object.keys(params.externalCase) + return Object.keys(externalCase) .filter((p) => mapping.get(p)?.actionType != null && mapping.get(p)?.actionType !== 'nothing') .map((p) => { const actionType = mapping.get(p)?.actionType ?? 'nothing'; return { key: p, - value: params.externalCase[p], + value: externalCase[p], actionType, pipes: actionType === 'append' ? [...defaultPipes, 'append'] : defaultPipes, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 6ba4d7cfc7de03..0020161789d716 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -32,6 +32,6 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); - actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities })); + actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index 3ae0e9db36de0c..709d490a5227f1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -88,7 +88,7 @@ mapping.set('summary', { }); const executorParams: ExecutorSubActionPushParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', externalId: 'incident-3', createdAt: '2020-04-27T10:59:46.202Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index b9225b043d526e..3de3926b7d8212 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -7,12 +7,12 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../case/utils'; +import * as utils from '../lib/axios_utils'; import { ExternalService } from '../case/types'; jest.mock('axios'); -jest.mock('../case/utils', () => { - const originalUtils = jest.requireActual('../case/utils'); +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); return { ...originalUtils, request: jest.fn(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index ff22b8368e7dd2..240b645c3a7dca 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -16,7 +16,7 @@ import { } from './types'; import * as i18n from './translations'; -import { getErrorMessage, request } from '../case/utils'; +import { request, getErrorMessage } from '../lib/axios_utils'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts new file mode 100644 index 00000000000000..4a52ae60bcddad --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -0,0 +1,105 @@ +/* + * 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 axios from 'axios'; +import { addTimeZoneToDate, throwIfNotAlive, request, patch, getErrorMessage } from './axios_utils'; +jest.mock('axios'); +const axiosMock = (axios as unknown) as jest.Mock; + +describe('addTimeZoneToDate', () => { + test('adds timezone with default', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z'); + expect(date).toBe('2020-04-14T15:01:55.456Z GMT'); + }); + + test('adds timezone correctly', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z', 'PST'); + expect(date).toBe('2020-04-14T15:01:55.456Z PST'); + }); +}); + +describe('throwIfNotAlive ', () => { + test('throws correctly when status is invalid', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json'); + }).toThrow('Instance is not alive.'); + }); + + test('throws correctly when content is invalid', () => { + expect(() => { + throwIfNotAlive(200, 'application/html'); + }).toThrow('Instance is not alive.'); + }); + + test('do NOT throws with custom validStatusCodes', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json', [404]); + }).not.toThrow('Instance is not alive.'); + }); +}); + +describe('request', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + }); + + test('it fetch correctly with defaults', async () => { + const res = await request({ axios, url: '/test' }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it fetch correctly', async () => { + const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it throws correctly', async () => { + axiosMock.mockImplementation(() => ({ + status: 404, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + + await expect(request({ axios, url: '/test' })).rejects.toThrow(); + }); +}); + +describe('patch', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + })); + }); + + test('it fetch correctly', async () => { + await patch({ axios, url: '/test', data: { id: '123' } }); + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); + }); +}); + +describe('getErrorMessage', () => { + test('it returns the correct error message', () => { + const msg = getErrorMessage('My connector name', 'An error has occurred'); + expect(msg).toBe('[Action][My connector name]: An error has occurred'); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts new file mode 100644 index 00000000000000..d527cf632bacec --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.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 { AxiosInstance, Method, AxiosResponse } from 'axios'; + +export const throwIfNotAlive = ( + status: number, + contentType: string, + validStatusCodes: number[] = [200, 201, 204] +) => { + if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { + throw new Error('Instance is not alive.'); + } +}; + +export const request = async ({ + axios, + url, + method = 'get', + data, + params, +}: { + axios: AxiosInstance; + url: string; + method?: Method; + data?: T; + params?: unknown; +}): Promise => { + const res = await axios(url, { method, data: data ?? {}, params }); + throwIfNotAlive(res.status, res.headers['content-type']); + return res; +}; + +export const patch = async ({ + axios, + url, + data, +}: { + axios: AxiosInstance; + url: string; + data: T; +}): Promise => { + return request({ + axios, + url, + method: 'patch', + data, + }); +}; + +export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { + return `${date} ${timezone}`; +}; + +export const getErrorMessage = (connector: string, msg: string) => { + return `[Action][${connector}]: ${msg}`; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 86a83188412712..7daf14e99f2546 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../case/api'; +import { Logger } from '../../../../../../src/core/server'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; +import { api } from './api'; +let mockedLogger: jest.Mocked; describe('api', () => { let externalService: jest.Mocked; @@ -24,7 +26,13 @@ describe('api', () => { describe('create incident', () => { test('it creates an incident', async () => { const params = { ...apiParams, externalId: null }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -46,7 +54,13 @@ describe('api', () => { test('it creates an incident without comments', async () => { const params = { ...apiParams, externalId: null, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -57,8 +71,14 @@ describe('api', () => { }); test('it calls createIncident correctly', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + const params = { ...apiParams, externalId: null, comments: undefined }; + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { @@ -71,53 +91,49 @@ describe('api', () => { expect(externalService.updateIncident).not.toHaveBeenCalled(); }); - test('it calls createComment correctly', async () => { + test('it calls updateIncident correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-1', - comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(2); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + comments: 'A comment', + description: + 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, - field: 'comments', + incidentId: 'incident-1', }); - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + comments: 'Another comment', + description: + 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, - field: 'comments', + incidentId: 'incident-1', }); }); }); describe('update incident', () => { test('it updates an incident', async () => { - const res = await api.pushToService({ externalService, mapping, params: apiParams }); + const res = await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-2', @@ -139,7 +155,13 @@ describe('api', () => { test('it updates an incident without comments', async () => { const params = { ...apiParams, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-2', @@ -151,7 +173,13 @@ describe('api', () => { test('it calls updateIncident correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', @@ -165,46 +193,35 @@ describe('api', () => { expect(externalService.createIncident).not.toHaveBeenCalled(); }); - test('it calls createComment correctly', async () => { + test('it calls updateIncident to create a comments correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-2', - comment: { - commentId: 'case-comment-1', - comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(3); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }, - field: 'comments', + incidentId: 'incident-3', }); - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-2', - comment: { - commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + comments: 'A comment', + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }, - field: 'comments', + incidentId: 'incident-2', }); }); }); @@ -231,7 +248,13 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -264,7 +287,13 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -295,7 +324,13 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -328,7 +363,13 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: {}, @@ -356,7 +397,13 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -387,7 +434,13 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -420,7 +473,13 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -451,7 +510,13 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -484,7 +549,13 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -515,8 +586,14 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); - expect(externalService.createComment).not.toHaveBeenCalled(); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 3db66e5884af4a..bd6f88f5efaa92 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -3,5 +3,145 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { flow } from 'lodash'; +import { + ExternalServiceParams, + PushToServiceApiHandlerArgs, + HandshakeApiHandlerArgs, + GetIncidentApiHandlerArgs, + ExternalServiceApi, +} from './types'; -export { api } from '../case/api'; +// TODO: to remove, need to support Case +import { transformers } from '../case/transformers'; +import { PushToServiceResponse, TransformFieldsArgs } from './case_types'; +import { prepareFieldsForTransformation } from '../case/utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: HandshakeApiHandlerArgs) => {}; +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: GetIncidentApiHandlerArgs) => {}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, + secrets, + logger, +}: PushToServiceApiHandlerArgs): Promise => { + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalId) { + try { + currentIncident = await externalService.getIncident(externalId); + } catch (ex) { + logger.debug( + `Retrieving Incident by id ${externalId} from ServiceNow was failed with exception: ${ex}` + ); + } + } + + let incident = {}; + // TODO: should be removed later but currently keep it for the Case implementation support + if (mapping) { + const fields = prepareFieldsForTransformation({ + externalCase: params.externalObject, + mapping, + defaultPipes, + }); + + incident = transformFields({ + params, + fields, + currentIncident, + }); + } else { + incident = { ...params, short_description: params.title, comments: params.comment }; + } + + if (updateIncident) { + res = await externalService.updateIncident({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createIncident({ + incident: { + ...incident, + caller_id: secrets.username, + }, + }); + } + + // TODO: should temporary keep comments for a Case usage + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping && + mapping.get('comments')?.actionType !== 'nothing' + ) { + res.comments = []; + + const fieldsKey = mapping.get('comments')?.target ?? 'comments'; + for (const currentComment of comments) { + await externalService.updateIncident({ + incidentId: res.id, + incident: { + ...incident, + [fieldsKey]: currentComment.comment, + }, + }); + res.comments = [ + ...(res.comments ?? []), + { + commentId: currentComment.commentId, + pushedDate: res.pushedDate, + }, + ]; + } + } + return res; +}; + +export const transformFields = ({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): Record => { + return fields.reduce((prev, cur) => { + const transform = flow(...cur.pipes.map((p) => transformers[p])); + return { + ...prev, + [cur.key]: transform({ + value: cur.value, + date: params.updatedAt ?? params.createdAt, + user: + (params.updatedBy != null + ? params.updatedBy.fullName + ? params.updatedBy.fullName + : params.updatedBy.username + : params.createdBy.fullName + ? params.createdBy.fullName + : params.createdBy.username) ?? '', + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value, + }; + }, {}); +}; + +export const api: ExternalServiceApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts new file mode 100644 index 00000000000000..2df8c8156cde8f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts @@ -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 { schema } from '@kbn/config-schema'; + +export const MappingActionType = schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), +]); + +export const MapRecordSchema = schema.object({ + source: schema.string(), + target: schema.string(), + actionType: MappingActionType, +}); + +export const IncidentConfigurationSchema = schema.object({ + mapping: schema.arrayOf(MapRecordSchema), +}); + +export const EntityInformation = { + createdAt: schema.maybe(schema.string()), + createdBy: schema.maybe(schema.any()), + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(schema.any()), +}; + +export const CommentSchema = schema.object({ + commentId: schema.string(), + comment: schema.string(), + ...EntityInformation, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts new file mode 100644 index 00000000000000..7e659125af7b2d --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { TypeOf } from '@kbn/config-schema'; +import { + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, +} from './schema'; +import { IncidentConfigurationSchema, MapRecordSchema } from './case_shema'; +import { + PushToServiceApiParams, + ExternalServiceIncidentResponse, + ExternalServiceParams, +} from './types'; + +export interface CreateCommentRequest { + [key: string]: string; +} + +export type IncidentConfiguration = TypeOf; +export type MapRecord = TypeOf; + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface PipedField { + key: string; + value: string; + actionType: string; + pipes: string[]; +} + +export interface TransformFieldsArgs { + params: PushToServiceApiParams; + fields: PipedField[]; + currentIncident?: ExternalServiceParams; +} + +export interface TransformerArgs { + value: string; + date?: string; + user?: string; + previousValue?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index dbb536d2fa53de..e62ca465f30f86 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -4,24 +4,99 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createConnector } from '../case/utils'; +import { curry } from 'lodash'; +import { schema } from '@kbn/config-schema'; -import { api } from './api'; -import { config } from './config'; import { validate } from './validators'; -import { createExternalService } from './service'; import { ExternalIncidentServiceConfiguration, ExternalIncidentServiceSecretConfiguration, -} from '../case/schema'; - -export const getActionType = createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: ExternalIncidentServiceConfiguration, - secrets: ExternalIncidentServiceSecretConfiguration, - }, -}); + ExecutorParamsSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { createExternalService } from './service'; +import { api } from './api'; +import { ExecutorParams, ExecutorSubActionPushParams } from './types'; +import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; + +// TODO: to remove, need to support Case +import { buildMap, mapParams } from '../case/utils'; +import { PushToServiceResponse } from './case_types'; + +interface GetActionTypeParams { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +} + +// action type definition +export function getActionType(params: GetActionTypeParams): ActionType { + const { logger, configurationUtilities } = params; + return { + id: '.servicenow', + minimumLicenseRequired: 'platinum', + name: i18n.NAME, + validate: { + config: schema.object(ExternalIncidentServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(ExternalIncidentServiceSecretConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor: curry(executor)({ logger }), + }; +} + +// action executor + +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions +): Promise { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data: PushToServiceResponse | null = null; + + const externalService = createExternalService({ + config, + secrets, + }); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction !== 'pushToService') { + const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + + const { comments, externalId, ...restParams } = pushToServiceParams; + const mapping = config.incidentConfiguration + ? buildMap(config.incidentConfiguration.mapping) + : null; + const externalObject = + config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalObject }, + secrets, + logger, + }); + + logger.debug(`response push to service for incident id: ${data.id}`); + } + + return { status: 'ok', data: data ?? {}, actionId }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 37228380910b3d..5f22fcd4fdc851 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExternalService, - PushToServiceApiParams, - ExecutorSubActionPushParams, - MapRecord, -} from '../case/types'; +import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; +import { MapRecord } from './case_types'; const createMock = (): jest.Mocked => { const service = { @@ -35,22 +31,9 @@ const createMock = (): jest.Mocked => { url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }) ), - createComment: jest.fn(), + findIncidents: jest.fn(), }; - service.createComment.mockImplementationOnce(() => - Promise.resolve({ - commentId: 'case-comment-1', - pushedDate: '2020-03-10T12:24:20.000Z', - }) - ); - - service.createComment.mockImplementationOnce(() => - Promise.resolve({ - commentId: 'case-comment-2', - pushedDate: '2020-03-10T12:24:20.000Z', - }) - ); return service; }; @@ -81,7 +64,7 @@ mapping.set('short_description', { }); const executorParams: ExecutorSubActionPushParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', externalId: 'incident-3', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -89,6 +72,10 @@ const executorParams: ExecutorSubActionPushParams = { updatedBy: { fullName: 'Elastic User', username: 'elastic' }, title: 'Incident title', description: 'Incident description', + comment: 'test-alert comment', + severity: '1', + urgency: '2', + impact: '1', comments: [ { commentId: 'case-comment-1', @@ -111,7 +98,7 @@ const executorParams: ExecutorSubActionPushParams = { const apiParams: PushToServiceApiParams = { ...executorParams, - externalCase: { short_description: 'Incident title', description: 'Incident description' }, + externalObject: { short_description: 'Incident title', description: 'Incident description' }, }; export { externalServiceMock, mapping, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts new file mode 100644 index 00000000000000..82afebaaee445b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.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 { schema } from '@kbn/config-schema'; +import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from './case_shema'; + +export const ExternalIncidentServiceConfiguration = { + apiUrl: schema.string(), + // TODO: to remove - set it optional for the current stage to support Case ServiceNow implementation + incidentConfiguration: schema.nullable(IncidentConfigurationSchema), + isCaseOwned: schema.maybe(schema.boolean()), +}; + +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); + +export const ExternalIncidentServiceSecretConfiguration = { + password: schema.string(), + username: schema.string(), +}; + +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), +]); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + savedObjectId: schema.string(), + title: schema.string(), + description: schema.nullable(schema.string()), + comment: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), + urgency: schema.nullable(schema.string()), + impact: schema.nullable(schema.string()), + // TODO: remove later - need for support Case push multiple comments + comments: schema.maybe(schema.arrayOf(CommentSchema)), + ...EntityInformation, +}); + +export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ + externalId: schema.string(), +}); + +// Reserved for future implementation +export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getIncident'), + subActionParams: ExecutorSubActionGetIncidentParamsSchema, + }), + schema.object({ + subAction: schema.literal('handshake'), + subActionParams: ExecutorSubActionHandshakeParamsSchema, + }), + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index f65cd5430560ed..07d60ec9f7a056 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -7,12 +7,12 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../case/utils'; -import { ExternalService } from '../case/types'; +import * as utils from '../lib/axios_utils'; +import { ExternalService } from './types'; jest.mock('axios'); -jest.mock('../case/utils', () => { - const originalUtils = jest.requireActual('../case/utils'); +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); return { ...originalUtils, request: jest.fn(), @@ -198,58 +198,22 @@ describe('ServiceNow service', () => { '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred' ); }); - }); - - describe('createComment', () => { test('it creates the comment correctly', async () => { patchMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + data: { result: { sys_id: '11', number: 'INC011', sys_updated_on: '2020-03-10 12:24:20' } }, })); - const res = await service.createComment({ + const res = await service.updateIncident({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: 'comment-1', }); expect(res).toEqual({ - commentId: 'comment-1', + title: 'INC011', + id: '11', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=11', }); }); - - test('it should call request with correct arguments', async () => { - patchMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, - })); - - await service.createComment({ - incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'my_field', - }); - - expect(patchMock).toHaveBeenCalledWith({ - axios, - url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', - data: { my_field: 'comment' }, - }); - }); - - test('it should throw an error', async () => { - patchMock.mockImplementation(() => { - throw new Error('An error has occurred'); - }); - - expect( - service.createComment({ - incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', - }) - ).rejects.toThrow( - '[Action][ServiceNow]: Unable to create comment at incident with id 1. Error: An error has occurred' - ); - }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 541fefce2f2ff5..2b5204af2eb7d6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -6,21 +6,14 @@ import axios from 'axios'; -import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; -import { addTimeZoneToDate, patch, request, getErrorMessage } from '../case/utils'; +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; import * as i18n from './translations'; -import { - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, - CreateIncidentRequest, - UpdateIncidentRequest, - CreateCommentRequest, -} from './types'; +import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; +import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; -const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; @@ -37,7 +30,6 @@ export const createExternalService = ({ } const incidentUrl = `${url}/${INCIDENT_URL}`; - const commentUrl = `${url}/${COMMENT_URL}`; const axiosInstance = axios.create({ auth: { username, password }, }); @@ -61,13 +53,29 @@ export const createExternalService = ({ } }; + const findIncidents = async (params?: Record) => { + try { + const res = await request({ + axios: axiosInstance, + url: incidentUrl, + params, + }); + + return res.data.result.length > 0 ? { ...res.data.result } : undefined; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to find incidents by query. Error: ${error.message}`) + ); + } + }; + const createIncident = async ({ incident }: ExternalServiceParams) => { try { - const res = await request({ + const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, method: 'post', - data: { ...incident }, + data: { ...(incident as Record) }, }); return { @@ -85,10 +93,10 @@ export const createExternalService = ({ const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { try { - const res = await patch({ + const res = await patch({ axios: axiosInstance, url: `${incidentUrl}/${incidentId}`, - data: { ...incident }, + data: { ...(incident as Record) }, }); return { @@ -107,32 +115,10 @@ export const createExternalService = ({ } }; - const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { - try { - const res = await patch({ - axios: axiosInstance, - url: `${commentUrl}/${incidentId}`, - data: { [field]: comment.comment }, - }); - - return { - commentId: comment.commentId, - pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; - } catch (error) { - throw new Error( - getErrorMessage( - i18n.NAME, - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - }; - return { getIncident, createIncident, updateIncident, - createComment, + findIncidents, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 3d6138169c4cc2..05c7d805a18525 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -6,6 +6,22 @@ import { i18n } from '@kbn/i18n'; -export const NAME = i18n.translate('xpack.actions.builtin.case.servicenowTitle', { +export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { defaultMessage: 'ServiceNow', }); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.configuration.apiWhitelistError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); + +// TODO: remove when Case mappings will be removed +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.servicenow.configuration.emptyMapping', + { + defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', + } +); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index d8476b7dca54a5..0db9b6642ea5cd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -4,18 +4,97 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - ExternalIncidentServiceConfiguration as ServiceNowPublicConfigurationType, - ExternalIncidentServiceSecretConfiguration as ServiceNowSecretConfigurationType, -} from '../case/types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ -export interface CreateIncidentRequest { - summary: string; - description: string; -} +import { TypeOf } from '@kbn/config-schema'; +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { IncidentConfigurationSchema } from './case_shema'; +import { PushToServiceResponse } from './case_types'; +import { Logger } from '../../../../../../src/core/server'; -export type UpdateIncidentRequest = Partial; +export type ServiceNowPublicConfigurationType = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +export type ServiceNowSecretConfigurationType = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; export interface CreateCommentRequest { [key: string]: string; } + +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +export type IncidentConfiguration = TypeOf; + +export interface ExternalServiceCredentials { + config: Record; + secrets: Record; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export type ExternalServiceParams = Record; + +export interface ExternalService { + getIncident: (id: string) => Promise; + createIncident: (params: ExternalServiceParams) => Promise; + updateIncident: (params: ExternalServiceParams) => Promise; + findIncidents: (params?: Record) => Promise; +} + +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalObject: Record; +} + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map | null; +} + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + secrets: Record; + logger: Logger; +} + +export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionGetIncidentParams; +} + +export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionHandshakeParams; +} + +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index 7226071392bc63..65bbe9aea8119b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -4,8 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; -import { ExternalServiceValidation } from '../case/types'; +import { isEmpty } from 'lodash'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExternalServiceValidation, +} from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ServiceNowPublicConfigurationType +) => { + if ( + configObject.incidentConfiguration !== null && + isEmpty(configObject.incidentConfiguration.mapping) + ) { + return i18n.MAPPING_EMPTY; + } + + try { + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ServiceNowSecretConfigurationType +) => {}; export const validate: ExternalServiceValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 283196373fe9f7..67b296d2ba1979 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -130,7 +130,7 @@ export const ServiceConnectorCommentParamsRt = rt.type({ }); export const ServiceConnectorCaseParamsRt = rt.type({ - caseId: rt.string, + savedObjectId: rt.string, createdAt: rt.string, createdBy: ServiceConnectorUserParams, externalId: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index 819d4110e168d1..e912c661439b2f 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -27,5 +27,6 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; +export const SERVICENOW_ACTION_TYPE_ID = '.servicenow'; export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira']; diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index 4aa6725159043d..b02f53bcd174a8 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -31,7 +31,7 @@ export const getActions = (): FindActionResult[] => [ actionTypeId: '.servicenow', name: 'ServiceNow', config: { - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -51,6 +51,7 @@ export const getActions = (): FindActionResult[] => [ ], }, apiUrl: 'https://dev102283.service-now.com', + isCaseOwned: true, }, isPreconfigured: false, referencedByCount: 0, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index d86e1777e920d0..28e75dd2f8c328 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -11,6 +11,7 @@ import { wrapError } from '../../utils'; import { CASE_CONFIGURE_CONNECTORS_URL, SUPPORTED_CONNECTORS, + SERVICENOW_ACTION_TYPE_ID, } from '../../../../../common/constants'; /* @@ -31,8 +32,12 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter((action) => - SUPPORTED_CONNECTORS.includes(action.actionTypeId) + const results = (await actionsClient.getAll()).filter( + (action) => + SUPPORTED_CONNECTORS.includes(action.actionTypeId) && + // Need this filtering temporary to display only Case owned ServiceNow connectors + (action.actionTypeId !== SERVICENOW_ACTION_TYPE_ID || + (action.actionTypeId === SERVICENOW_ACTION_TYPE_ID && action.config!.isCaseOwned)) ); return response.ok({ body: results }); } catch (error) { diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index f070431a34f21e..91a5aa5c88beb8 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -125,7 +125,7 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, + mapping: connectors[0].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-1', connectorName: 'unchanged', @@ -213,7 +213,7 @@ describe('ConfigureCases', () => { jest.clearAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, + mapping: connectors[1].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-2', connectorName: 'unchanged', @@ -332,7 +332,7 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, + mapping: connectors[0].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-1', connectorName: 'My connector', @@ -399,7 +399,7 @@ describe('closure options', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, + mapping: connectors[0].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-1', connectorName: 'My connector', @@ -435,7 +435,7 @@ describe('user interactions', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, + mapping: connectors[1].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-2', connectorName: 'unchanged', diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx index 256c8893be9416..43922462cd092a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx @@ -198,6 +198,7 @@ const ConfigureCasesComponent: React.FC = ({ userC capabilities: application.capabilities, reloadConnectors, docLinks, + consumer: 'case', }} > { updateCase, }; const sampleServiceRequestData = { - caseId: pushedCase.id, + savedObjectId: pushedCase.id, createdAt: pushedCase.createdAt, createdBy: serviceConnectorUser, comments: [ diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx index 0d8a4c04ca7cd7..346390bd2a49f7 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx @@ -171,7 +171,7 @@ export const formatServiceRequestData = ( const actualExternalService = caseServices[connectorId] ?? null; return { - caseId, + savedObjectId: caseId, createdAt, createdBy: { fullName: createdBy.fullName ?? null, diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts index d8b55665f77683..0b19e4177f5c27 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connector as serviceNowConnectorConfig } from './servicenow/config'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceNowConnectorConfiguration } from '../../../../../triggers_actions_ui/public/common'; import { connector as jiraConnectorConfig } from './jira/config'; import { ConnectorConfiguration } from './types'; export const connectorsConfiguration: Record = { - '.servicenow': serviceNowConnectorConfig, + '.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration, '.jira': jiraConnectorConfig, }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts index 2ce61bef49c5ed..83b07a2905ef0f 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getActionType as serviceNowActionType } from './servicenow'; export { getActionType as jiraActionType } from './jira'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/flyout.tsx deleted file mode 100644 index 1e5abbab46a06f..00000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/flyout.tsx +++ /dev/null @@ -1,87 +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 { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import * as i18n from './translations'; -import { ConnectorFlyoutFormProps } from '../types'; -import { ServiceNowActionConnector } from './types'; -import { withConnectorFlyout } from '../components/connector_flyout'; - -const ServiceNowConnectorForm: React.FC> = ({ - errors, - action, - onChangeSecret, - onBlurSecret, -}) => { - const { username, password } = action.secrets; - const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; - const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; - - return ( - <> - - - - onChangeSecret('username', evt.target.value)} - onBlur={() => onBlurSecret('username')} - /> - - - - - - - - onChangeSecret('password', evt.target.value)} - onBlur={() => onBlurSecret('password')} - /> - - - - - ); -}; - -export const ServiceNowConnectorFlyout = withConnectorFlyout({ - ConnectorFormComponent: ServiceNowConnectorForm, - secretKeys: ['username', 'password'], - connectorActionTypeId: '.servicenow', -}); - -// eslint-disable-next-line import/no-default-export -export { ServiceNowConnectorFlyout as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/index.tsx deleted file mode 100644 index c9c5298365e817..00000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/index.tsx +++ /dev/null @@ -1,47 +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 { lazy } from 'react'; -import { - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../triggers_actions_ui/public/types'; -import { connector } from './config'; -import { createActionType } from '../utils'; -import logo from './logo.svg'; -import { ServiceNowActionConnector } from './types'; -import * as i18n from './translations'; - -interface Errors { - username: string[]; - password: string[]; -} - -const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { - const errors: Errors = { - username: [], - password: [], - }; - - if (!action.secrets.username) { - errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; - } - - if (!action.secrets.password) { - errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; - } - - return { errors }; -}; - -export const getActionType = createActionType({ - id: connector.id, - iconClass: logo, - selectMessage: i18n.SERVICENOW_DESC, - actionTypeTitle: connector.name, - validateConnector, - actionConnectorFields: lazy(() => import('./flyout')), -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts deleted file mode 100644 index b3e58dcd5b6be1..00000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts +++ /dev/null @@ -1,30 +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'; - -export * from '../translations'; - -export const SERVICENOW_DESC = i18n.translate( - 'xpack.securitySolution.case.connectors.servicenow.selectMessageText', - { - defaultMessage: 'Push or update Security case data to a new incident in ServiceNow', - } -); - -export const SERVICENOW_TITLE = i18n.translate( - 'xpack.securitySolution.case.connectors.servicenow.actionTypeTitle', - { - defaultMessage: 'ServiceNow', - } -); - -export const MAPPING_FIELD_SHORT_DESC = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldShortDescription', - { - defaultMessage: 'Short Description', - } -); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/types.ts deleted file mode 100644 index b4a80e28c8d154..00000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/types.ts +++ /dev/null @@ -1,22 +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. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, -} from '../../../../../../actions/server/builtin_action_types/servicenow/types'; - -export { ServiceNowFieldsType } from '../../../../../../case/common/api/connectors'; - -export * from '../types'; - -export interface ServiceNowActionConnector { - config: ServiceNowPublicConfigurationType; - secrets: ServiceNowSecretConfigurationType; -} diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 65121327b40b99..18072c25e6dde9 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -22,7 +22,7 @@ import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { serviceNowActionType, jiraActionType } from './common/lib/connectors'; +import { jiraActionType } from './common/lib/connectors'; import { PluginSetup, PluginStart, @@ -74,7 +74,6 @@ export class Plugin implements IPlugin { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c12b1366746b0f..d97e5ec2ced60e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3940,7 +3940,6 @@ "xpack.actions.builtin.case.configuration.emptyMapping": "[casesConfiguration.mapping]:空以外の値が必要ですが空でした", "xpack.actions.builtin.case.connectorApiNullError": "コネクター[apiUrl]が必要です", "xpack.actions.builtin.case.jiraTitle": "Jira", - "xpack.actions.builtin.case.servicenowTitle": "ServiceNow", "xpack.actions.builtin.email.errorSendingErrorMessage": "エラー送信メールアドレス", "xpack.actions.builtin.emailTitle": "メール", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "エラーインデックス作成ドキュメント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f68a245acbc316..9a3bd8f615a47b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3943,7 +3943,6 @@ "xpack.actions.builtin.case.configuration.emptyMapping": "[casesConfiguration.mapping]:应为非空,但却为空", "xpack.actions.builtin.case.connectorApiNullError": "需要指定连接器 [apiUrl]", "xpack.actions.builtin.case.jiraTitle": "Jira", - "xpack.actions.builtin.case.servicenowTitle": "ServiceNow", "xpack.actions.builtin.email.errorSendingErrorMessage": "发送电子邮件时出错", "xpack.actions.builtin.emailTitle": "电子邮件", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "索引文档时出错", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 5a25f7b94050e4..4b6e596b8d6577 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -1295,6 +1295,7 @@ Then this dependencies will be used to embed Actions form or register your own a defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'} actionTypes={ALOWED_BY_PLUGIN_ACTION_TYPES} toastNotifications={toastNotifications} + consumer={initialAlert.consumer} /> ); }; @@ -1317,6 +1318,7 @@ interface ActionAccordionFormProps { actionTypes?: ActionType[]; messageVariables?: string[]; defaultActionMessage?: string; + consumer: string; } ``` @@ -1334,6 +1336,7 @@ interface ActionAccordionFormProps { |actionTypes|Optional property, which allowes to define a list of available actions specific for a current plugin.| |actionTypes|Optional property, which allowes to define a list of variables for action 'message' property.| |defaultActionMessage|Optional property, which allowes to define a message value for action with 'message' property.| +|consumer|Name of the plugin that creates an action.| AlertsContextProvider value options: @@ -1425,7 +1428,7 @@ const connector = { toastNotifications: toastNotifications, actionTypeRegistry: triggers_actions_ui.actionTypeRegistry, capabilities: capabilities, - docLinks, + docLinks, }} > Promise; + consumer: string; } ``` @@ -1479,6 +1483,7 @@ export interface ActionsConnectorsContextValue { |capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.| |toastNotifications|Toast messages.| |reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.| +|consumer|Optional name of the plugin that creates an action.| ## Embed the Edit Connector flyout within any Kibana plugin diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 8f49fa46dd54e6..c241997e99dd7b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -12,6 +12,7 @@ import { getPagerDutyActionType } from './pagerduty'; import { getWebhookActionType } from './webhook'; import { TypeRegistry } from '../../type_registry'; import { ActionTypeModel } from '../../../types'; +import { getServiceNowActionType } from './servicenow'; export function registerBuiltInActionTypes({ actionTypeRegistry, @@ -24,4 +25,5 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType()); actionTypeRegistry.register(getPagerDutyActionType()); actionTypeRegistry.register(getWebhookActionType()); + actionTypeRegistry.register(getServiceNowActionType()); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx new file mode 100644 index 00000000000000..52b881a1eb75fa --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx @@ -0,0 +1,141 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFormRow, EuiFlexItem, EuiFlexGroup, EuiSuperSelectOption } from '@elastic/eui'; +import styled from 'styled-components'; + +import { FieldMappingRow } from './field_mapping_row'; +import * as i18n from './translations'; + +import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; +import { ThirdPartyField as ConnectorConfigurationThirdPartyField } from './types'; +import { CasesConfigurationMapping } from '../types'; +import { connectorConfiguration } from '../config'; +import { createDefaultMapping } from '../servicenow_connectors'; + +const FieldRowWrapper = styled.div` + margin-top: 8px; + font-size: 14px; +`; + +const actionTypeOptions: Array> = [ + { + value: 'nothing', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_NOTHING}, + 'data-test-subj': 'edit-update-option-nothing', + }, + { + value: 'overwrite', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_OVERWRITE}, + 'data-test-subj': 'edit-update-option-overwrite', + }, + { + value: 'append', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_APPEND}, + 'data-test-subj': 'edit-update-option-append', + }, +]; + +const getThirdPartyOptions = ( + caseField: string, + thirdPartyFields: Record +): Array> => + (Object.keys(thirdPartyFields) as string[]).reduce>>( + (acc, key) => { + if (thirdPartyFields[key].validSourceFields.includes(caseField)) { + return [ + ...acc, + { + value: key, + inputDisplay: {thirdPartyFields[key].label}, + 'data-test-subj': `dropdown-mapping-${key}`, + }, + ]; + } + return acc; + }, + [ + { + value: 'not_mapped', + inputDisplay: i18n.MAPPING_FIELD_NOT_MAPPED, + 'data-test-subj': 'dropdown-mapping-not_mapped', + }, + ] + ); + +export interface FieldMappingProps { + disabled: boolean; + mapping: CasesConfigurationMapping[] | null; + connectorActionTypeId: string; + onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; +} + +const FieldMappingComponent: React.FC = ({ + disabled, + mapping, + onChangeMapping, + connectorActionTypeId, +}) => { + const onChangeActionType = useCallback( + (caseField: string, newActionType: string) => { + const myMapping = mapping ?? defaultMapping; + onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [mapping] + ); + + const onChangeThirdParty = useCallback( + (caseField: string, newThirdPartyField: string) => { + const myMapping = mapping ?? defaultMapping; + onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [mapping] + ); + + const selectedConnector = connectorConfiguration ?? { fields: {} }; + const defaultMapping = useMemo(() => createDefaultMapping(selectedConnector.fields), [ + selectedConnector.fields, + ]); + + return ( + <> + + + + {i18n.FIELD_MAPPING_FIRST_COL} + + + {i18n.FIELD_MAPPING_SECOND_COL} + + + {i18n.FIELD_MAPPING_THIRD_COL} + + + + + {(mapping ?? defaultMapping).map((item) => ( + + ))} + + + ); +}; + +export const FieldMapping = React.memo(FieldMappingComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx new file mode 100644 index 00000000000000..beca8f1fbbc77a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx @@ -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 React, { useMemo } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiSuperSelect, + EuiIcon, + EuiSuperSelectOption, +} from '@elastic/eui'; + +import { capitalize } from 'lodash'; + +export interface RowProps { + id: string; + disabled: boolean; + securitySolutionField: string; + thirdPartyOptions: Array>; + actionTypeOptions: Array>; + onChangeActionType: (caseField: string, newActionType: string) => void; + onChangeThirdParty: (caseField: string, newThirdPartyField: string) => void; + selectedActionType: string; + selectedThirdParty: string; +} + +const FieldMappingRowComponent: React.FC = ({ + id, + disabled, + securitySolutionField, + thirdPartyOptions, + actionTypeOptions, + onChangeActionType, + onChangeThirdParty, + selectedActionType, + selectedThirdParty, +}) => { + const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [ + securitySolutionField, + ]); + return ( + + + + + {securitySolutionFieldCapitalized} + + + + + + + + + + + + + + ); +}; + +export const FieldMappingRow = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts new file mode 100644 index 00000000000000..665ccbcfa114df --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts @@ -0,0 +1,190 @@ +/* + * 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 INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemTitle', + { + defaultMessage: 'Connect to external incident management system', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemDesc', + { + defaultMessage: + 'You may optionally connect Security cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemLabel', + { + defaultMessage: 'Incident management system', + } +); + +export const NO_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.noConnector', + { + defaultMessage: 'No connector selected', + } +); + +export const ADD_NEW_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.addNewConnector', + { + defaultMessage: 'Add new connector', + } +); + +export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsTitle', + { + defaultMessage: 'Case Closures', + } +); + +export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsDesc', + { + defaultMessage: + 'Define how you wish Security cases to be closed. Automated case closures require an established connection to an external incident management system.', + } +); + +export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsLabel', + { + defaultMessage: 'Case closure options', + } +); + +export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsManual', + { + defaultMessage: 'Manually close Security cases', + } +); + +export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsNewIncident', + { + defaultMessage: + 'Automatically close Security cases when pushing new incident to external system', + } +); + +export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsClosedIncident', + { + defaultMessage: 'Automatically close Security cases when incident is closed in external system', + } +); + +export const FIELD_MAPPING_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingTitle', + { + defaultMessage: 'Field mappings', + } +); + +export const FIELD_MAPPING_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingDesc', + { + defaultMessage: + 'Map Security case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', + } +); + +export const FIELD_MAPPING_FIRST_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingFirstCol', + { + defaultMessage: 'Security case field', + } +); + +export const FIELD_MAPPING_SECOND_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingSecondCol', + { + defaultMessage: 'External incident field', + } +); + +export const FIELD_MAPPING_THIRD_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingThirdCol', + { + defaultMessage: 'On edit and update', + } +); + +export const FIELD_MAPPING_EDIT_NOTHING = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditNothing', + { + defaultMessage: 'Nothing', + } +); + +export const FIELD_MAPPING_EDIT_OVERWRITE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditOverwrite', + { + defaultMessage: 'Overwrite', + } +); + +export const FIELD_MAPPING_EDIT_APPEND = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditAppend', + { + defaultMessage: 'Append', + } +); + +export const CANCEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.cancelButton', + { + defaultMessage: 'Cancel', + } +); + +export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.warningTitle', + { + defaultMessage: 'Warning', + } +); + +export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.warningMessage', + { + defaultMessage: + 'The selected connector has been deleted. Either select a different connector or create a new one.', + } +); + +export const MAPPING_FIELD_NOT_MAPPED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped', + { + defaultMessage: 'Not mapped', + } +); + +export const UPDATE_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.updateConnector', + { + defaultMessage: 'Update connector', + } +); + +export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => { + return i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.updateSelectedConnector', + { + values: { connectorName }, + defaultMessage: 'Update { connectorName }', + } + ); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts similarity index 50% rename from x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts index 70d53ab79f6310..6cd2200e1dc74b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalServiceConfiguration } from '../case/types'; -import * as i18n from './translations'; +import { ActionType } from '../../../../../types'; -export const config: ExternalServiceConfiguration = { - id: '.servicenow', - name: i18n.NAME, - minimumLicenseRequired: 'platinum', -}; +export { ActionType }; + +export interface ThirdPartyField { + label: string; + validSourceFields: string[]; + defaultSourceField: string; + defaultActionType: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts new file mode 100644 index 00000000000000..a173d905153020 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.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 { CasesConfigurationMapping } from '../types'; + +export const setActionTypeToMapping = ( + caseField: string, + newActionType: string, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => { + const findItemIndex = mapping.findIndex((item) => item.source === caseField); + + if (findItemIndex >= 0) { + return [ + ...mapping.slice(0, findItemIndex), + { ...mapping[findItemIndex], actionType: newActionType }, + ...mapping.slice(findItemIndex + 1), + ]; + } + + return [...mapping]; +}; + +export const setThirdPartyToMapping = ( + caseField: string, + newThirdPartyField: string, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => + mapping.map((item) => { + if (item.source !== caseField && item.target === newThirdPartyField) { + return { ...item, target: 'not_mapped' }; + } else if (item.source === caseField) { + return { ...item, target: newThirdPartyField }; + } + return item; + }); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts similarity index 91% rename from x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/config.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts index 35c677c9574e36..7f810cf5eb38fd 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from './types'; import * as i18n from './translations'; import logo from './logo.svg'; -export const connector: ConnectorConfiguration = { +export const connectorConfiguration = { id: '.servicenow', name: i18n.SERVICENOW_TITLE, logo, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/index.ts new file mode 100644 index 00000000000000..65bb3ae4f5a37b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/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 { getActionType as getServiceNowActionType } from './servicenow'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.svg similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/logo.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx new file mode 100644 index 00000000000000..5e70bc20f5c514 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { ServiceNowActionConnector } from './types'; + +const ACTION_TYPE_ID = '.servicenow'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('servicenow connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + name: 'ServiceNow', + isPreconfigured: false, + config: { + apiUrl: 'https://dev94428.service-now.com/', + }, + } as ServiceNowActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: [], + username: [], + password: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = ({ + secrets: { + username: 'user', + }, + id: '.servicenow', + actionTypeId: '.servicenow', + name: 'servicenow', + config: {}, + } as unknown) as ServiceNowActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: ['URL is required.'], + username: [], + password: ['Password is required.'], + }, + }); + }); +}); + +describe('servicenow action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + subActionParams: { title: 'some title {{test}}' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { title: [] }, + }); + }); + + test('params validation fails when body is not valid', () => { + const actionParams = { + subActionParams: { title: '' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + title: ['Title is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx new file mode 100644 index 00000000000000..0f7b83ed84fb47 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -0,0 +1,67 @@ +/* + * 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 { lazy } from 'react'; +import { ValidationResult, ActionTypeModel } from '../../../../types'; +import { connectorConfiguration } from './config'; +import logo from './logo.svg'; +import { ServiceNowActionConnector, ServiceNowActionParams } from './types'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; + +const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + apiUrl: new Array(), + username: new Array(), + password: new Array(), + }; + validationResult.errors = errors; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (action.config.apiUrl && !isValidUrl(action.config.apiUrl, 'https:')) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + if (!action.secrets.username) { + errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; + } + + if (!action.secrets.password) { + errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; + } + + return validationResult; +}; + +export function getActionType(): ActionTypeModel< + ServiceNowActionConnector, + ServiceNowActionParams +> { + return { + id: connectorConfiguration.id, + iconClass: logo, + selectMessage: i18n.SERVICENOW_DESC, + actionTypeTitle: connectorConfiguration.name, + validateConnector, + actionConnectorFields: lazy(() => import('./servicenow_connectors')), + validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + title: new Array(), + }; + validationResult.errors = errors; + if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { + errors.title.push(i18n.TITLE_REQUIRED); + } + return validationResult; + }, + actionParamsFields: lazy(() => import('./servicenow_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx new file mode 100644 index 00000000000000..452d9c288926e1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DocLinksStart } from 'kibana/public'; +import ServiceNowConnectorFields from './servicenow_connectors'; +import { ServiceNowActionConnector } from './types'; + +describe('ServiceNowActionConnectorFields renders', () => { + test('alerting servicenow connector fields is rendered', () => { + const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + name: 'webhook', + config: { + apiUrl: 'https://test/', + }, + } as ServiceNowActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> + ); + expect( + wrapper.find('[data-test-subj="connector-servicenow-username-form-input"]').length > 0 + ).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 + ).toBeTruthy(); + }); + + test('case specific servicenow connector fields is rendered', () => { + const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + isPreconfigured: false, + name: 'servicenow', + config: { + apiUrl: 'https://test/', + incidentConfiguration: { mapping: [] }, + isCaseOwned: true, + }, + } as ServiceNowActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> + ); + expect(wrapper.find('[data-test-subj="case-servicenow-mappings"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx new file mode 100644 index 00000000000000..a5c4849cb63d91 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -0,0 +1,182 @@ +/* + * 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, { useCallback } from 'react'; + +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import * as i18n from './translations'; +import { ServiceNowActionConnector, CasesConfigurationMapping } from './types'; +import { connectorConfiguration } from './config'; +import { FieldMapping } from './case_mappings/field_mapping'; + +const ServiceNowConnectorFields: React.FC> = ({ action, editActionSecrets, editActionConfig, errors, consumer }) => { + // TODO: remove incidentConfiguration later, when Case ServiceNow will move their fields to the level of action execution + const { apiUrl, incidentConfiguration, isCaseOwned } = action.config; + const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + const { username, password } = action.secrets; + + const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; + const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; + + // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + if (consumer === 'case') { + if (isEmpty(mapping)) { + editActionConfig('incidentConfiguration', { + mapping: createDefaultMapping(connectorConfiguration.fields as any), + }); + } + if (!isCaseOwned) { + editActionConfig('isCaseOwned', true); + } + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('incidentConfiguration', { + ...action.config.incidentConfiguration, + mapping: newMapping, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [action.config] + ); + + return ( + <> + + + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('username', evt.target.value)} + onBlur={() => { + if (!username) { + editActionSecrets('username', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('password', evt.target.value)} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + {isCaseOwned && ( // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + <> + + + + + + + + )} + + ); +}; + +export const createDefaultMapping = (fields: Record): CasesConfigurationMapping[] => + Object.keys(fields).map( + (key) => + ({ + source: fields[key].defaultSourceField, + target: key, + actionType: fields[key].defaultActionType, + } as CasesConfigurationMapping) + ); + +// eslint-disable-next-line import/no-default-export +export { ServiceNowConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx new file mode 100644 index 00000000000000..57d50cf7e5bdda --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import ServiceNowParamsFields from './servicenow_params'; + +describe('ServiceNowParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + subAction: 'pushToService', + subActionParams: { + title: 'sn title', + description: 'some description', + comment: 'comment for sn', + severity: '1', + urgency: '2', + impact: '3', + savedObjectId: '123', + externalId: null, + }, + }; + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + /> + ); + expect(wrapper.find('[data-test-subj="urgencySelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( + '1' + ); + expect(wrapper.find('[data-test-subj="impactSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="incidentDescriptionTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="incidentCommentTextArea"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx new file mode 100644 index 00000000000000..67070b6dc8907a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx @@ -0,0 +1,262 @@ +/* + * 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, { Fragment, useEffect } from 'react'; +import { EuiFormRow, EuiTextArea } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { ActionParamsProps } from '../../../../types'; +import { AddMessageVariables } from '../../add_message_variables'; +import { ServiceNowActionParams } from './types'; + +const ServiceNowParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, errors, messageVariables }) => { + const { title, description, comment, severity, urgency, impact, savedObjectId } = + actionParams.subActionParams || {}; + const selectOptions = [ + { + value: '1', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel', + { + defaultMessage: 'High', + } + ), + }, + { + value: '2', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel', + { + defaultMessage: 'Medium', + } + ), + }, + { + value: '3', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel', + { + defaultMessage: 'Low', + } + ), + }, + ]; + + const editSubActionProperty = (key: string, value: {}) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }; + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + if (!savedObjectId && messageVariables?.find((variable) => variable === 'alertId')) { + editSubActionProperty('savedObjectId', '{{alertId}}'); + } + if (!urgency) { + editSubActionProperty('urgency', '3'); + } + if (!impact) { + editSubActionProperty('impact', '3'); + } + if (!severity) { + editSubActionProperty('severity', '3'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [title, description, comment, severity, impact, urgency]); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editSubActionProperty( + paramsProperty, + ((actionParams as any).subActionParams[paramsProperty] ?? '').concat(` {{${variable}}}`) + ); + }; + + return ( + + +

Incident

+
+ + + { + editSubActionProperty('urgency', e.target.value); + }} + /> + + + + + + { + editSubActionProperty('severity', e.target.value); + }} + /> + + + + + { + editSubActionProperty('impact', e.target.value); + }} + /> + + + + + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel', + { + defaultMessage: 'Short description', + } + )} + labelAppend={ + onSelectMessageVariable('title', variable)} + paramsProperty="title" + /> + } + > + 0 && title !== undefined} + value={title || ''} + onChange={(e: React.ChangeEvent) => { + editSubActionProperty('title', e.target.value); + }} + onBlur={() => { + if (!title) { + editSubActionProperty('title', ''); + } + }} + /> + + + onSelectMessageVariable('description', variable) + } + paramsProperty="description" + /> + } + > + { + editSubActionProperty('description', e.target.value); + }} + onBlur={() => { + if (!description) { + editSubActionProperty('description', ''); + } + }} + /> + + + onSelectMessageVariable('comment', variable) + } + paramsProperty="comment" + /> + } + > + { + editSubActionProperty('comment', e.target.value); + }} + onBlur={() => { + if (!comment) { + editSubActionProperty('comment', ''); + } + }} + /> + +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts new file mode 100644 index 00000000000000..f5670f432d4d45 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -0,0 +1,133 @@ +/* + * 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 SERVICENOW_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText', + { + defaultMessage: 'Push or update data to a new incident in ServiceNow.', + } +); + +export const SERVICENOW_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.actionTypeTitle', + { + defaultMessage: 'ServiceNow', + } +); + +export const API_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const USERNAME_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel', + { + defaultMessage: 'Username', + } +); + +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField', + { + defaultMessage: 'Username is required.', + } +); + +export const PASSWORD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel', + { + defaultMessage: 'Password', + } +); + +export const PASSWORD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField', + { + defaultMessage: 'Password is required.', + } +); + +export const API_TOKEN_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiTokenTextFieldLabel', + { + defaultMessage: 'Api token', + } +); + +export const API_TOKEN_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiTokenTextField', + { + defaultMessage: 'Api token is required.', + } +); + +export const EMAIL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.emailTextFieldLabel', + { + defaultMessage: 'Email', + } +); + +export const EMAIL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField', + { + defaultMessage: 'Email is required.', + } +); + +export const MAPPING_FIELD_SHORT_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldShortDescription', + { + defaultMessage: 'Short Description', + } +); + +export const MAPPING_FIELD_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldDescription', + { + defaultMessage: 'Description', + } +); + +export const MAPPING_FIELD_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldComments', + { + defaultMessage: 'Comments', + } +); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField', + { + defaultMessage: 'Description is required.', + } +); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', + { + defaultMessage: 'Title is required.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts new file mode 100644 index 00000000000000..92252efc3a41c8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.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. + */ + +export interface ServiceNowActionConnector { + config: ServiceNowConfig; + secrets: ServiceNowSecrets; +} + +export interface ServiceNowActionParams { + subAction: string; + subActionParams: { + savedObjectId: string; + title: string; + description: string; + comment: string; + externalId: string | null; + severity: string; + urgency: string; + impact: string; + }; +} + +interface IncidentConfiguration { + mapping: CasesConfigurationMapping[]; +} + +interface ServiceNowConfig { + apiUrl: string; + incidentConfiguration?: IncidentConfiguration; + isCaseOwned?: boolean; +} + +interface ServiceNowSecrets { + username: string; + password: string; +} + +// to remove +export interface CasesConfigurationMapping { + source: string; + target: string; + actionType: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx index 11934d3af3ceba..311ae587bbe13e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -12,7 +12,7 @@ import { SlackActionConnector } from '../types'; const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => { +>> = ({ action, editActionSecrets, errors }) => { const { webhookUrl } = action.secrets; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx index f8a9085a88656e..d78930344a6736 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx @@ -19,6 +19,7 @@ export interface ActionsConnectorsContextValue { capabilities: ApplicationStart['capabilities']; reloadConnectors?: () => Promise; docLinks: DocLinksStart; + consumer?: string; } const ActionsConnectorsContext = createContext(null as any); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts index 9d628adc1db6be..e954fb5c7617bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts @@ -3,7 +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 { throwIfAbsent, throwIfIsntContained } from './value_validators'; +import { throwIfAbsent, throwIfIsntContained, isValidUrl } from './value_validators'; import uuid from 'uuid'; describe('throwIfAbsent', () => { @@ -79,3 +79,17 @@ describe('throwIfIsntContained', () => { ).toEqual(values); }); }); + +describe('isValidUrl', () => { + test('verifies invalid url', () => { + expect(isValidUrl('this is not a url')).toBeFalsy(); + }); + + test('verifies valid url any protocol', () => { + expect(isValidUrl('https://www.elastic.co/')).toBeTruthy(); + }); + + test('verifies valid url with specific protocol', () => { + expect(isValidUrl('https://www.elastic.co/', 'https:')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts index 7ee73590864065..4942e6328097dd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts @@ -31,3 +31,15 @@ export function throwIfIsntContained( return values; }; } + +export const isValidUrl = (urlString: string, protocol?: string) => { + try { + const urlObject = new URL(urlString); + if (protocol === undefined || urlObject.protocol === protocol) { + return true; + } + return false; + } catch (err) { + return false; + } +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index 489cdf167b2838..813f3598a748d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -53,6 +53,7 @@ interface ActionConnectorProps { http: HttpSetup; actionTypeRegistry: TypeRegistry; docLinks: DocLinksStart; + consumer?: string; } export const ActionConnectorForm = ({ @@ -64,6 +65,7 @@ export const ActionConnectorForm = ({ http, actionTypeRegistry, docLinks, + consumer, }: ActionConnectorProps) => { const setActionProperty = (key: string, value: any) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); @@ -170,6 +172,7 @@ export const ActionConnectorForm = ({ editActionSecrets={setActionSecretsProperty} http={http} docLinks={docLinks} + consumer={consumer} /> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 7db6b5145f8950..c21cce4cc4b62c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -149,6 +149,16 @@ describe('action_form', () => { config: {}, isPreconfigured: false, }, + { + secrets: {}, + id: '.servicenow', + actionTypeId: '.servicenow', + name: 'Non consumer connector', + config: { + isCaseOwned: true, + }, + isPreconfigured: false, + }, ]); const mocks = coreMock.createSetup(); const [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 201852ddeee488..7f400ee9a5db1e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -45,6 +45,7 @@ import { TypeRegistry } from '../../type_registry'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; +import { ServiceNowConnectorConfiguration } from '../../../common'; interface ActionAccordionFormProps { actions: AlertAction[]; @@ -131,7 +132,14 @@ export const ActionForm = ({ try { setIsLoadingConnectors(true); const loadedConnectors = await loadConnectors({ http }); - setConnectors(loadedConnectors); + setConnectors( + loadedConnectors.filter( + (action) => + action.actionTypeId !== ServiceNowConnectorConfiguration.id || + (action.actionTypeId === ServiceNowConnectorConfiguration.id && + !action.config.isCaseOwned) + ) + ); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 2dd1f83749372d..60ec0cfa6955ee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -52,6 +52,7 @@ export const ConnectorAddFlyout = ({ actionTypeRegistry, reloadConnectors, docLinks, + consumer, } = useActionsConnectorsContext(); const [actionType, setActionType] = useState(undefined); const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); @@ -117,6 +118,7 @@ export const ConnectorAddFlyout = ({ actionTypeRegistry={actionTypeRegistry} http={http} docLinks={docLinks} + consumer={consumer} /> ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 1d19f436950c7a..67c836fc12cf77 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -40,6 +40,7 @@ interface ConnectorAddModalProps { >; capabilities: ApplicationStart['capabilities']; docLinks: DocLinksStart; + consumer?: string; } export const ConnectorAddModal = ({ @@ -52,6 +53,7 @@ export const ConnectorAddModal = ({ actionTypeRegistry, capabilities, docLinks, + consumer, }: ConnectorAddModalProps) => { let hasErrors = false; const initialConnector = { @@ -164,6 +166,7 @@ export const ConnectorAddModal = ({ actionTypeRegistry={actionTypeRegistry} docLinks={docLinks} http={http} + consumer={consumer} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index cbbbbfaea7ea30..68fd8b65f1a41b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -48,6 +48,7 @@ export const ConnectorEditFlyout = ({ actionTypeRegistry, reloadConnectors, docLinks, + consumer, } = useActionsConnectorsContext(); const canSave = hasSaveActionsCapability(capabilities); const closeFlyout = useCallback(() => setEditFlyoutVisibility(false), [setEditFlyoutVisibility]); @@ -185,6 +186,7 @@ export const ConnectorEditFlyout = ({ actionTypeRegistry={actionTypeRegistry} http={http} docLinks={docLinks} + consumer={consumer} /> ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 09d94e2418cb83..40505ac3fe76c7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import * as React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { ScopedHistory } from 'kibana/public'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ActionsConnectorsList } from './actions_connectors_list'; import { coreMock, scopedHistoryMock } from '../../../../../../../../src/core/public/mocks'; import { ReactWrapper } from 'enzyme'; @@ -27,7 +27,7 @@ const actionTypeRegistry = actionTypeRegistryMock.create(); describe('actions_connectors_list component empty', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -75,25 +75,29 @@ describe('actions_connectors_list component empty', () => { }; actionTypeRegistry.has.mockReturnValue(true); + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); + } - await waitForRender(wrapper); - }); - - it('renders empty prompt', () => { + it('renders empty prompt', async () => { + await setup(); expect(wrapper.find('EuiEmptyPrompt')).toHaveLength(1); expect( wrapper.find('[data-test-subj="createFirstActionButton"]').find('EuiButton') ).toHaveLength(1); }); - test('if click create button should render ConnectorAddFlyout', () => { + test('if click create button should render ConnectorAddFlyout', async () => { + await setup(); wrapper.find('[data-test-subj="createFirstActionButton"]').first().simulate('click'); expect(wrapper.find('ConnectorAddFlyout')).toHaveLength(1); }); @@ -102,7 +106,7 @@ describe('actions_connectors_list component empty', () => { describe('actions_connectors_list component with items', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -181,29 +185,34 @@ describe('actions_connectors_list component with items', () => { alertTypeRegistry: {} as any, }; + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); - await waitForRender(wrapper); - expect(loadAllActions).toHaveBeenCalled(); - }); + } - it('renders table of connectors', () => { + it('renders table of connectors', async () => { + await setup(); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(3); }); - it('renders table with preconfigured connectors', () => { + it('renders table with preconfigured connectors', async () => { + await setup(); expect(wrapper.find('[data-test-subj="preConfiguredTitleMessage"]')).toHaveLength(2); }); test('if select item for edit should render ConnectorEditFlyout', async () => { + await setup(); await wrapper.find('[data-test-subj="edit1"]').first().simulate('click'); expect(wrapper.find('ConnectorEditFlyout')).toHaveLength(1); @@ -213,7 +222,7 @@ describe('actions_connectors_list component with items', () => { describe('actions_connectors_list component empty with show only capability', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -264,18 +273,21 @@ describe('actions_connectors_list component empty with show only capability', () alertTypeRegistry: {} as any, }; + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); + } - await waitForRender(wrapper); - }); - - it('renders no permissions to create connector', () => { + it('renders no permissions to create connector', async () => { + await setup(); expect(wrapper.find('[defaultMessage="No permissions to create connector"]')).toHaveLength(1); expect(wrapper.find('[data-test-subj="createActionButton"]')).toHaveLength(0); }); @@ -284,7 +296,7 @@ describe('actions_connectors_list component empty with show only capability', () describe('actions_connectors_list with show only capability', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -350,18 +362,21 @@ describe('actions_connectors_list with show only capability', () => { alertTypeRegistry: {} as any, }; + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); + } - await waitForRender(wrapper); - }); - - it('renders table of connectors with delete button disabled', () => { + it('renders table of connectors with delete button disabled', async () => { + await setup(); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); wrapper.find('EuiTableRow').forEach((elem) => { @@ -375,7 +390,7 @@ describe('actions_connectors_list with show only capability', () => { describe('actions_connectors_list component with disabled items', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -448,20 +463,23 @@ describe('actions_connectors_list component with disabled items', () => { alertTypeRegistry: {} as any, }; + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); - await waitForRender(wrapper); - expect(loadAllActions).toHaveBeenCalled(); - }); + } - it('renders table of connectors', () => { + it('renders table of connectors', async () => { + await setup(); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual( @@ -472,9 +490,3 @@ describe('actions_connectors_list component with disabled items', () => { ); }); }); - -async function waitForRender(wrapper: ReactWrapper) { - await Promise.resolve(); - await Promise.resolve(); - wrapper.update(); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 5d52896cc628f8..0e0691960729df 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ServiceNowConnectorConfiguration } from '../../../../common'; import { useAppDependencies } from '../../../app_context'; import { loadAllActions, loadActionTypes, deleteActions } from '../../../lib/action_connector_api'; import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout'; @@ -118,7 +119,14 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { setIsLoadingActions(true); try { const actionsResponse = await loadAllActions({ http }); - setActions(actionsResponse); + setActions( + actionsResponse.filter( + (action) => + action.actionTypeId !== ServiceNowConnectorConfiguration.id || + (action.actionTypeId === ServiceNowConnectorConfiguration.id && + !action.config.isCaseOwned) + ) + ); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index 94089a274e79d9..9dd3fd787f8605 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -5,3 +5,5 @@ */ export * from './expression_items'; + +export { connectorConfiguration as ServiceNowConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config'; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 52179dd35767ca..a4a13d7ec849c6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -32,6 +32,7 @@ export interface ActionConnectorFieldsProps { errors: IErrorObject; docLinks: DocLinksStart; http?: HttpSetup; + consumer?: string; } export interface ActionParamsProps { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts index 3356b3e3d58288..a451edea76d83e 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts @@ -38,15 +38,27 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - casesConfiguration: { mapping: [...mapping] }, + incidentConfiguration: { mapping: [...mapping] }, + isCaseOwned: true, }, secrets: { password: 'elastic', username: 'changeme', }, params: { - comments: 'hello cool service now incident', - short_description: 'this is a cool service now incident', + savedObjectId: '123', + title: 'a title', + description: 'a description', + comment: 'test-alert comment', + severity: '1', + urgency: '2', + impact: '1', + comments: [ + { + commentId: '456', + comment: 'first comment', + }, + ], }, }; describe('servicenow', () => { @@ -68,7 +80,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + incidentConfiguration: { ...mockServiceNow.config.incidentConfiguration }, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts index 8a675ec10aa8c1..e2f31da1c8064f 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts @@ -25,6 +25,10 @@ export function initPlugin(router: IRouter, path: string) { short_description: schema.string(), description: schema.maybe(schema.string()), comments: schema.maybe(schema.string()), + caller_id: schema.string(), + severity: schema.string({ defaultValue: '1' }), + urgency: schema.string({ defaultValue: '1' }), + impact: schema.string({ defaultValue: '1' }), }), }, }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 093f09c24bad36..19206ce6810006 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -50,7 +50,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { subAction: 'pushToService', subActionParams: { - caseId: '123', + savedObjectId: '123', title: 'a title', description: 'a description', createdAt: '2020-03-13T08:34:53.450Z', @@ -361,12 +361,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', }); }); }); - it('should handle failing with a simulated success without caseId', async () => { + it('should handle failing with a simulated success without savedObjectId', async () => { await supertest .post(`/api/actions/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') @@ -379,7 +379,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', }); }); }); @@ -392,7 +392,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { ...mockJira.params, subActionParams: { - caseId: 'success', + savedObjectId: 'success', }, }, }) @@ -415,7 +415,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { ...mockJira.params, subActionParams: { - caseId: 'success', + savedObjectId: 'success', title: 'success', }, }, @@ -440,7 +440,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { ...mockJira.params, subActionParams: { ...mockJira.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, @@ -468,7 +468,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { ...mockJira.params, subActionParams: { ...mockJira.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, @@ -496,7 +496,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { ...mockJira.params, subActionParams: { ...mockJira.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 531a362fa2babb..8205b75cabed5e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -40,7 +40,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - casesConfiguration: { mapping }, + incidentConfiguration: { mapping }, + isCaseOwned: true, }, secrets: { password: 'elastic', @@ -49,18 +50,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { params: { subAction: 'pushToService', subActionParams: { - caseId: '123', - title: 'a title', - description: 'a description', + savedObjectId: '123', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - externalId: null, comments: [ { commentId: '456', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -68,6 +63,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { updatedBy: null, }, ], + description: 'a description', + externalId: null, + title: 'a title', + updatedAt: '2020-06-17T04:37:45.147Z', + updatedBy: { fullName: null, username: 'elastic' }, }, }, }; @@ -93,7 +93,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -106,7 +107,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, }); @@ -121,7 +123,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, }); }); @@ -155,7 +158,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: 'http://servicenow.mynonexistent.com', - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -179,7 +183,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, }) .expect(400) @@ -193,7 +198,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { + it('should create a servicenow action without incidentConfiguration', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -202,18 +207,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', - }); - }); + .expect(200); }); it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { @@ -225,7 +223,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { mapping: [] }, + incidentConfiguration: { mapping: [] }, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -235,7 +234,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', }); }); }); @@ -249,7 +248,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -258,6 +257,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, ], }, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -276,7 +276,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }); @@ -332,12 +333,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', }); }); }); - it('should handle failing with a simulated success without caseId', async () => { + it('should handle failing with a simulated success without savedObjectId', async () => { await supertest .post(`/api/actions/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') @@ -350,7 +351,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', }); }); }); @@ -363,7 +364,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { params: { ...mockServiceNow.params, subActionParams: { - caseId: 'success', + savedObjectId: 'success', }, }, }) @@ -378,30 +379,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - subActionParams: { - caseId: 'success', - title: 'success', - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', - }); - }); - }); - it('should handle failing with a simulated success without commentId', async () => { await supertest .post(`/api/actions/action/${simulatedActionId}/_execute`) @@ -411,7 +388,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { ...mockServiceNow.params, subActionParams: { ...mockServiceNow.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, @@ -425,7 +402,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', }); }); }); @@ -439,7 +416,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { ...mockServiceNow.params, subActionParams: { ...mockServiceNow.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, @@ -453,35 +430,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', - }); - }); - }); - - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - subActionParams: { - ...mockServiceNow.params.subActionParams, - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments.0.comment]: expected value of type [string] but got [undefined]', }); }); }); From 610bff1269df5546261231f5acde686957061fea Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Mon, 6 Jul 2020 19:52:58 -0400 Subject: [PATCH 21/57] [Security Solution] Change default index pattern (#70797) * [Security Solution] Change default index pattern Add `logs-*` to the Security Solution default index pattern. This should allow the app to recognize events from the Elastic Endpoint. --- x-pack/plugins/security_solution/common/constants.ts | 1 + .../detection_engine/rules/fetch_index_patterns.test.tsx | 6 +++++- .../__snapshots__/drag_drop_context_wrapper.test.tsx.snap | 1 + .../event_details/__snapshots__/event_details.test.tsx.snap | 2 ++ .../public/common/containers/source/index.test.tsx | 6 ++++-- .../public/overview/components/overview_host/index.test.tsx | 1 + .../overview/components/overview_network/index.test.tsx | 1 + .../timeline/__snapshots__/timeline.test.tsx.snap | 1 + .../body/column_headers/__snapshots__/index.test.tsx.snap | 1 + .../__snapshots__/suricata_row_renderer.test.tsx.snap | 1 + .../renderers/zeek/__snapshots__/zeek_details.test.tsx.snap | 1 + .../zeek/__snapshots__/zeek_row_renderer.test.tsx.snap | 1 + 12 files changed, 20 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index d32d9f01d61aec..a34a76361f7991 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -61,6 +61,7 @@ export const DEFAULT_INDEX_PATTERN = [ 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ]; /** This Kibana Advanced Setting enables the `Security news` feed widget */ diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx index 79d5886f8845f1..c282a204f19a5d 100644 --- a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -354,6 +354,7 @@ describe('useFetchIndexPatterns', () => { 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], name: 'event.end', searchable: true, @@ -370,6 +371,7 @@ describe('useFetchIndexPatterns', () => { 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], indicesExists: true, indexPatterns: { @@ -415,7 +417,8 @@ describe('useFetchIndexPatterns', () => { { name: 'source.port', searchable: true, type: 'long', aggregatable: true }, { name: 'event.end', searchable: true, type: 'date', aggregatable: true }, ], - title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + title: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*,logs-*', }, }, result.current[1], @@ -449,6 +452,7 @@ describe('useFetchIndexPatterns', () => { 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], indicesExists: false, isLoading: false, diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap index 07cbd6dfe03706..0c96d0320d1987 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap @@ -371,6 +371,7 @@ exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] = "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 33ed6a8c87b5fd..408a4c74e930fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -379,6 +379,7 @@ exports[`EventDetails rendering should match snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, @@ -1071,6 +1072,7 @@ In other use cases the message field can be used to concatenate different values "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index 69e4ac615ebf2a..b9daba9a409418 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -28,7 +28,8 @@ describe('Index Fields & Browser Fields', () => { errorMessage: null, indexPattern: { fields: [], - title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + title: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*,logs-*', }, indicesExist: true, loading: true, @@ -57,7 +58,8 @@ describe('Index Fields & Browser Fields', () => { browserFields: mockBrowserFields, indexPattern: { fields: mockIndexFields, - title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + title: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*,logs-*', }, loading: false, errorMessage: null, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index 2b21385004a739..bb9fd73d2df8ea 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -60,6 +60,7 @@ const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], inspect: false, }, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index 42c80b6b115bd4..0f6fce1486ee7d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -75,6 +75,7 @@ const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], inspect: false, }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap index 012cfd66317de7..7baefaa6ab9516 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -476,6 +476,7 @@ exports[`Timeline rendering renders correctly against snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 9508e3f18a3484..efd99e781d827c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -384,6 +384,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap index 93b3046b57ed61..cba4b9aa72a250 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap @@ -373,6 +373,7 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index 0a60c8facff9c0..e1000637147a81 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -371,6 +371,7 @@ exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap index 460ad35b476783..d4c80441e60377 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap @@ -373,6 +373,7 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, From 438e905800feecd0b76dff18c75305a4355294d5 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 6 Jul 2020 17:35:47 -0700 Subject: [PATCH 22/57] Added UI validation when creating a Webhook connector with invalid URL (#70025) * Added UI validation when creating a Webhook connector with invalid URL * fixed tests * Fixed due to comments * fixed type check and extended error message for invalid URL * Fixed whitelisting of URL * fixed failing tests * fixed str --- .../builtin_action_types/webhook.test.ts | 11 ++++++++ .../server/builtin_action_types/webhook.ts | 14 +++++++++- .../webhook/webhook.test.tsx | 27 ++++++++++++++++++- .../builtin_action_types/webhook/webhook.tsx | 12 +++++++++ .../webhook/webhook_connectors.tsx | 1 + 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 6daf15208f4d95..53b17f58d6e187 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -114,6 +114,17 @@ describe('config validation', () => { }); }); + test('config validation failed when a url is invalid', () => { + const config: Record = { + url: 'example.com/do-something', + }; + expect(() => { + validateConfig(actionType, config); + }).toThrowErrorMatchingInlineSnapshot( + '"error validating action type config: error configuring webhook action: unable to parse url: TypeError: Invalid URL: example.com/do-something"' + ); + }); + test('config validation passes when valid headers are provided', () => { // any for testing // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 4a34fea7621643..0b8b27b2789286 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -85,8 +85,20 @@ function validateActionTypeConfig( configurationUtilities: ActionsConfigurationUtilities, configObject: ActionTypeConfigType ) { + let url: URL; try { - configurationUtilities.ensureWhitelistedUri(configObject.url); + url = new URL(configObject.url); + } catch (err) { + return i18n.translate('xpack.actions.builtin.webhook.webhookConfigurationErrorNoHostname', { + defaultMessage: 'error configuring webhook action: unable to parse url: {err}', + values: { + err, + }, + }); + } + + try { + configurationUtilities.ensureWhitelistedUri(url.toString()); } catch (whitelistError) { return i18n.translate('xpack.actions.builtin.webhook.webhookConfigurationError', { defaultMessage: 'error configuring webhook action: {message}', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx index 3413465d70d935..337c1f0f18a932 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx @@ -40,7 +40,7 @@ describe('webhook connector validation', () => { isPreconfigured: false, config: { method: 'PUT', - url: 'http:\\test', + url: 'http://test.com', headers: { 'content-type': 'text' }, }, } as WebhookActionConnector; @@ -77,6 +77,31 @@ describe('webhook connector validation', () => { }, }); }); + + test('connector validation fails when url in config is not valid', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + url: 'invalid.url', + }, + } as WebhookActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + url: ['URL is invalid.'], + method: [], + user: [], + password: [], + }, + }); + }); }); describe('webhook action params validation', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx index 9f33e4491233a7..2c51b21d700342 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx @@ -7,6 +7,7 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { ActionTypeModel, ValidationResult } from '../../../../types'; import { WebhookActionParams, WebhookActionConnector } from '../types'; +import { isValidUrl } from '../../../lib/value_validators'; export function getActionType(): ActionTypeModel { return { @@ -43,6 +44,17 @@ export function getActionType(): ActionTypeModel 0 && url !== undefined} fullWidth value={url || ''} + placeholder="https:// or http://" data-test-subj="webhookUrlText" onChange={(e) => { editActionConfig('url', e.target.value); From c5eab1021fe5f5502faa8c9e99a120df1dcc2351 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jul 2020 23:09:26 -0400 Subject: [PATCH 23/57] Revert "reenable regression and classification functional tests (#70661)" (#70908) This reverts commit a9b543d9bca049bda4fe03977f23de9561765873. --- .../apps/ml/data_frame_analytics/classification_creation.ts | 4 ++-- .../apps/ml/data_frame_analytics/regression_creation.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 5c750e119063e2..4a79610cadbde6 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - - describe('classification creation', function () { + // flaky test https://github.com/elastic/kibana/issues/70455 + describe.skip('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 9ddf2dfc482694..33f0ee9cd99ac7 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - - describe('regression creation', function () { + // flaky test https://github.com/elastic/kibana/issues/70455 + describe.skip('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); From 79e5a07bdc8333fa0a02d5605f957561746f6628 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 7 Jul 2020 05:16:58 +0100 Subject: [PATCH 24/57] skip flaky suite (#70906) --- .../apps/ml/data_frame_analytics/outlier_detection_creation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 500825f7d9d314..65e6dc9b4ea740 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -11,7 +11,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('outlier detection creation', function () { + // Flaky: https://github.com/elastic/kibana/issues/70906 + describe.skip('outlier detection creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/ihp_outlier'); await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); From 468201acf3bd4c8704c126cc9ddc84eece189f5a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 7 Jul 2020 05:22:48 +0100 Subject: [PATCH 25/57] skip flaky suite (#67814) --- .../cypress/integration/alerts_detection_rules_custom.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 51c29c15a8097b..684570450aa05d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -64,7 +64,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { ALERTS_URL } from '../urls/navigation'; -describe('Detection rules, custom', () => { +// Flaky: https://github.com/elastic/kibana/issues/67814 +describe.skip('Detection rules, custom', () => { before(() => { esArchiverLoad('custom_rule_with_timeline'); }); From f62f3e372786b1dde84e9b3b7972458da4501f22 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 7 Jul 2020 05:34:02 +0100 Subject: [PATCH 26/57] skip flaky suite (#70885) --- x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 068ef48b095e1e..e2f7960f9d856a 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('jobs cloning supported by UI form', function () { + // Flaky: https://github.com/elastic/kibana/issues/70885 + describe.skip('jobs cloning supported by UI form', function () { const testDataList: Array<{ suiteTitle: string; archive: string; From 4257afad1b65dc8ff715d99e7a325fa55c2d3e53 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 7 Jul 2020 07:27:12 +0200 Subject: [PATCH 27/57] Adapt expected response of advanced settings feature control for cloud tests (#70793) --- .../advanced_settings/feature_controls.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts index 77e23bd74cc220..7a0d0fe2f5d487 100644 --- a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts +++ b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts @@ -21,9 +21,16 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }; const expectResponse = (result: any) => { - expect(result.error).to.be(undefined); - expect(result.response).not.to.be(undefined); - expect(result.response).to.have.property('statusCode', 200); + if (result.response && result.response.statusCode === 400) { + // expect a change of telemetry settings to fail in cloud environment + expect(result.response.body.message).to.be( + '{"error":"Not allowed to change Opt-in Status."}' + ); + } else { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 200); + } }; async function saveAdvancedSetting(username: string, password: string, spaceId?: string) { From dfeb60b5ee8c116564d8fcd796b73b36e724a9fa Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Tue, 7 Jul 2020 09:21:00 +0200 Subject: [PATCH 28/57] moving indexPattern.delete() to indexPatterns.delete(indexPattern) (#70430) --- ...lugins-data-public.indexpattern.destroy.md | 15 ----------- ...plugin-plugins-data-public.indexpattern.md | 1 - .../index_patterns/index_pattern.ts | 26 +++---------------- .../index_patterns/index_patterns.test.ts | 10 +++++++ .../index_patterns/index_patterns.ts | 9 +++++++ src/plugins/data/public/public.api.md | 2 -- .../edit_index_pattern/edit_index_pattern.tsx | 20 +++++++++----- .../plugins/index_patterns/server/plugin.ts | 3 +-- 8 files changed, 36 insertions(+), 50 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.destroy.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.destroy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.destroy.md deleted file mode 100644 index 3a8e1b9dae5a62..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.destroy.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [destroy](./kibana-plugin-plugins-data-public.indexpattern.destroy.md) - -## IndexPattern.destroy() method - -Signature: - -```typescript -destroy(): Promise<{}> | undefined; -``` -Returns: - -`Promise<{}> | undefined` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index bc999a3bb48e34..a37f1153589222 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -39,7 +39,6 @@ export declare class IndexPattern implements IIndexPattern | [\_fetchFields()](./kibana-plugin-plugins-data-public.indexpattern._fetchfields.md) | | | | [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | | | [create(allowOverride)](./kibana-plugin-plugins-data-public.indexpattern.create.md) | | | -| [destroy()](./kibana-plugin-plugins-data-public.indexpattern.destroy.md) | | | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-public.indexpattern.getaggregationrestrictions.md) | | | | [getComputedFields()](./kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md) | | | | [getFieldByName(name)](./kibana-plugin-plugins-data-public.indexpattern.getfieldbyname.md) | | | diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index dab11ad0ce29a1..2acb9d5f767ad7 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -224,7 +224,7 @@ export class IndexPattern implements IIndexPattern { this.sourceFilters = spec.sourceFilters; // ignoring this because the same thing happens elsewhere but via _.assign - // @ts-ignore + // @ts-expect-error this.fields = spec.fields || []; this.typeMeta = spec.typeMeta; this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { @@ -473,21 +473,8 @@ export class IndexPattern implements IIndexPattern { async create(allowOverride: boolean = false) { const _create = async (duplicateId?: string) => { if (duplicateId) { - const duplicatePattern = new IndexPattern(duplicateId, { - getConfig: this.getConfig, - savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: this.patternCache, - fieldFormats: this.fieldFormats, - onNotification: this.onNotification, - onError: this.onError, - uiSettingsValues: { - shortDotsEnable: this.shortDotsEnable, - metaFields: this.metaFields, - }, - }); - - await duplicatePattern.destroy(); + this.patternCache.clear(duplicateId); + await this.savedObjectsClient.delete(savedObjectType, duplicateId); } const body = this.prepBody(); @@ -634,11 +621,4 @@ export class IndexPattern implements IIndexPattern { toString() { return '' + this.toJSON(); } - - destroy() { - if (this.id) { - this.patternCache.clear(this.id); - return this.savedObjectsClient.delete(savedObjectType, this.id); - } - } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index 2eb9744fc16b32..a1842d31479c09 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -53,6 +53,7 @@ describe('IndexPatterns', () => { Array> > ); + savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise); indexPatterns = new IndexPatternsService({ uiSettings: ({ @@ -98,4 +99,13 @@ describe('IndexPatterns', () => { await indexPatterns.getFields(['id', 'title'], true); expect(savedObjectsClient.find).toHaveBeenCalledTimes(3); }); + + test('deletes the index pattern', async () => { + const id = '1'; + const indexPattern = await indexPatterns.get(id); + + expect(indexPattern).toBeDefined(); + await indexPatterns.delete(id); + expect(indexPattern).not.toBe(await indexPatterns.get(id)); + }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index ef03ca8fe2d144..a07ffaf92aea57 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -228,6 +228,15 @@ export class IndexPatternsService { return indexPattern.init(); } + + /** + * Deletes an index pattern from .kibana index + * @param indexPatternId: Id of kibana Index Pattern to delete + */ + async delete(indexPatternId: string) { + indexPatternCache.clear(indexPatternId); + return this.savedObjectsClient.delete('index-pattern', indexPatternId); + } } export type IndexPatternsContract = PublicMethodsOf; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 670b40e7d94722..2b18584bcd781e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -988,8 +988,6 @@ export class IndexPattern implements IIndexPattern { // (undocumented) create(allowOverride?: boolean): Promise; // (undocumented) - destroy(): Promise<{}> | undefined; - // (undocumented) _fetchFields(): Promise; // (undocumented) fieldFormatMap: any; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index eab8b2c231c9ca..090c72d319f8c2 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -83,9 +83,14 @@ const confirmModalOptionsDelete = { export const EditIndexPattern = withRouter( ({ indexPattern, history, location }: EditIndexPatternProps) => { - const { uiSettings, indexPatternManagementStart, overlays, savedObjects, chrome } = useKibana< - IndexPatternManagmentContext - >().services; + const { + uiSettings, + indexPatternManagementStart, + overlays, + savedObjects, + chrome, + data, + } = useKibana().services; const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); const [conflictedFields, setConflictedFields] = useState( indexPattern.fields.filter((field) => field.type === 'conflict') @@ -138,10 +143,11 @@ export const EditIndexPattern = withRouter( uiSettings.set('defaultIndex', otherPatterns[0].id); } } - - Promise.resolve(indexPattern.destroy()).then(function () { - history.push(''); - }); + if (indexPattern.id) { + Promise.resolve(data.indexPatterns.delete(indexPattern.id)).then(function () { + history.push(''); + }); + } } overlays.openConfirm('', confirmModalOptionsDelete).then((isConfirmed) => { diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index ffc70136ccffa6..d6a4fdd67b0a11 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -96,8 +96,7 @@ export class IndexPatternsTestPlugin const [, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const service = await data.indexPatterns.indexPatternsServiceFactory(req); - const ip = await service.get(id); - await ip.destroy(); + await service.delete(id); return res.ok(); } ); From 77e40199b80254896766fe284ca074a8ef80742e Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 7 Jul 2020 09:22:09 +0200 Subject: [PATCH 29/57] [Uptime] Ping list body scroll (#70781) --- .../uptime/public/components/monitor/ping_list/expanded_row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx index e8ce3465f6fd86..67bef3e72929e0 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx @@ -47,7 +47,7 @@ const BodyDescription = ({ body }: { body: HttpResponseBody }) => { }; const BodyExcerpt = ({ content }: { content: string }) => - content ? {content} : null; + content ? {content} : null; export const PingListExpandedRowComponent = ({ ping }: Props) => { const listItems = []; From 053b922b7cacc264c2d6ec9fdb909dd893266261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 7 Jul 2020 09:58:00 +0200 Subject: [PATCH 30/57] [Composable template] Details panel + delete functionality (#70814) --- .../home/index_templates_tab.helpers.ts | 16 +- .../home/index_templates_tab.test.ts | 187 ++++++++-- .../common/lib/template_serialization.ts | 22 +- .../common/types/templates.ts | 6 + .../components/template_content_indicator.tsx | 12 +- .../template_form/template_form.tsx | 2 + .../template_details/index.ts | 7 - .../template_details/template_details.tsx | 330 ------------------ .../template_table/template_table.tsx | 21 +- .../template_details/tabs/tab_summary.tsx | 244 +++++++++---- .../template_details/template_details.tsx | 18 +- .../template_details_content.tsx | 324 +++++++++++++++++ .../home/template_list/template_list.tsx | 9 +- .../template_table/template_table.tsx | 166 +++++++-- .../sections/template_edit/template_edit.tsx | 4 +- .../server/lib/get_managed_templates.ts | 2 +- .../api/templates/register_delete_route.ts | 15 +- .../api/templates/register_get_routes.ts | 14 +- .../routes/api/templates/validate_schemas.ts | 2 + .../test/fixtures/template.ts | 6 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../management/index_management/templates.js | 32 ++ 23 files changed, 921 insertions(+), 522 deletions(-) delete mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts delete mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx create mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index 5eb4eaf6e2ca1b..0047e4c0294cb9 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -51,12 +51,15 @@ const createActions = (testBed: TestBed) => { find('reloadButton').simulate('click'); }; - const clickActionMenu = async (templateName: TemplateDeserialized['name']) => { + const clickActionMenu = (templateName: TemplateDeserialized['name']) => { const { component } = testBed; // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions" // The template name may contain a period (.) so we use bracket syntax for selector - component.find(`div[id="${templateName}-actions"] button`).simulate('click'); + act(() => { + component.find(`div[id="${templateName}-actions"] button`).simulate('click'); + }); + component.update(); }; const clickTemplateAction = ( @@ -68,12 +71,15 @@ const createActions = (testBed: TestBed) => { clickActionMenu(templateName); - component.find('.euiContextMenuItem').at(actions.indexOf(action)).simulate('click'); + act(() => { + component.find('.euiContextMenuItem').at(actions.indexOf(action)).simulate('click'); + }); + component.update(); }; - const clickTemplateAt = async (index: number) => { + const clickTemplateAt = async (index: number, isLegacy = false) => { const { component, table, router } = testBed; - const { rows } = table.getMetaData('legacyTemplateTable'); + const { rows } = table.getMetaData(isLegacy ? 'legacyTemplateTable' : 'templateTable'); const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); const { href } = templateLink.props(); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index fb3e16e5345cb6..1ec29f1c5b894b 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -63,6 +63,7 @@ describe('Index Templates tab', () => { }, }, }); + (template1 as any).hasSettings = true; const template2 = fixtures.getTemplate({ name: `b${getRandomString()}`, @@ -122,20 +123,22 @@ describe('Index Templates tab', () => { // Test composable table content tableCellsValues.forEach((row, i) => { - const template = templates[i]; - const { name, indexPatterns, priority, ilmPolicy, composedOf } = template; + const indexTemplate = templates[i]; + const { name, indexPatterns, priority, ilmPolicy, composedOf, template } = indexTemplate; + const hasContent = !!template.settings || !!template.mappings || !!template.aliases; const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; const composedOfString = composedOf ? composedOf.join(',') : ''; const priorityFormatted = priority ? priority.toString() : ''; expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + '', // Checkbox to select row name, indexPatterns.join(', '), ilmPolicyName, composedOfString, priorityFormatted, - 'M S A', // Mappings Settings Aliases badges + hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges '', // Column of actions ]); }); @@ -202,52 +205,101 @@ describe('Index Templates tab', () => { }); test('each row should have a link to the template details panel', async () => { - const { find, exists, actions } = testBed; + const { find, exists, actions, component } = testBed; + // Composable templates await actions.clickTemplateAt(0); + expect(exists('templateList')).toBe(true); + expect(exists('templateDetails')).toBe(true); + expect(find('templateDetails.title').text()).toBe(templates[0].name); + + // Close flyout + await act(async () => { + actions.clickCloseDetailsButton(); + }); + component.update(); + + await actions.clickTemplateAt(0, true); expect(exists('templateList')).toBe(true); expect(exists('templateDetails')).toBe(true); expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name); }); - test('template actions column should have an option to delete', () => { - const { actions, findAction } = testBed; - const [{ name: templateName }] = legacyTemplates; + describe('table row actions', () => { + describe('composable templates', () => { + test('should have an option to delete', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = templates; - actions.clickActionMenu(templateName); + actions.clickActionMenu(templateName); - const deleteAction = findAction('delete'); + const deleteAction = findAction('delete'); + expect(deleteAction.text()).toEqual('Delete'); + }); - expect(deleteAction.text()).toEqual('Delete'); - }); + test('should have an option to clone', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = templates; - test('template actions column should have an option to clone', () => { - const { actions, findAction } = testBed; - const [{ name: templateName }] = legacyTemplates; + actions.clickActionMenu(templateName); - actions.clickActionMenu(templateName); + const cloneAction = findAction('clone'); - const cloneAction = findAction('clone'); + expect(cloneAction.text()).toEqual('Clone'); + }); - expect(cloneAction.text()).toEqual('Clone'); - }); + test('should have an option to edit', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = templates; + + actions.clickActionMenu(templateName); - test('template actions column should have an option to edit', () => { - const { actions, findAction } = testBed; - const [{ name: templateName }] = legacyTemplates; + const editAction = findAction('edit'); + + expect(editAction.text()).toEqual('Edit'); + }); + }); + + describe('legacy templates', () => { + test('should have an option to delete', () => { + const { actions, findAction } = testBed; + const [{ name: legacyTemplateName }] = legacyTemplates; + + actions.clickActionMenu(legacyTemplateName); + + const deleteAction = findAction('delete'); + expect(deleteAction.text()).toEqual('Delete'); + }); + + test('should have an option to clone', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = legacyTemplates; + + actions.clickActionMenu(templateName); + + const cloneAction = findAction('clone'); + + expect(cloneAction.text()).toEqual('Clone'); + }); - actions.clickActionMenu(templateName); + test('should have an option to edit', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = legacyTemplates; - const editAction = findAction('edit'); + actions.clickActionMenu(templateName); - expect(editAction.text()).toEqual('Edit'); + const editAction = findAction('edit'); + + expect(editAction.text()).toEqual('Edit'); + }); + }); }); describe('delete index template', () => { test('should show a confirmation when clicking the delete template button', async () => { const { actions } = testBed; - const [{ name: templateName }] = legacyTemplates; + const [{ name: templateName }] = templates; await actions.clickTemplateAction(templateName, 'delete'); @@ -267,24 +319,29 @@ describe('Index Templates tab', () => { actions.toggleViewItem('system'); - const { name: systemTemplateName } = legacyTemplates[2]; + const { name: systemTemplateName } = templates[2]; await actions.clickTemplateAction(systemTemplateName, 'delete'); expect(exists('deleteSystemTemplateCallOut')).toBe(true); }); test('should send the correct HTTP request to delete an index template', async () => { - const { actions, table } = testBed; - const { rows } = table.getMetaData('legacyTemplateTable'); - - const templateId = rows[0].columns[2].value; + const { actions } = testBed; const [ { name: templateName, _kbnMeta: { isLegacy }, }, - ] = legacyTemplates; + ] = templates; + + httpRequestsMockHelpers.setDeleteTemplateResponse({ + results: { + successes: [templateName], + errors: [], + }, + }); + await actions.clickTemplateAction(templateName, 'delete'); const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]'); @@ -292,13 +349,68 @@ describe('Index Templates tab', () => { '[data-test-subj="confirmModalConfirmButton"]' ); + await act(async () => { + confirmButton!.click(); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + expect(latestRequest.method).toBe('POST'); + expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete_index_templates`); + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ + templates: [{ name: templates[0].name, isLegacy }], + }); + }); + }); + + describe('delete legacy index template', () => { + test('should show a confirmation when clicking the delete template button', async () => { + const { actions } = testBed; + const [{ name: templateName }] = legacyTemplates; + + await actions.clickTemplateAction(templateName, 'delete'); + + // We need to read the document "body" as the modal is added there and not inside + // the component DOM tree. + expect( + document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]') + ).not.toBe(null); + + expect( + document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]')!.textContent + ).toContain('Delete template'); + }); + + test('should show a warning message when attempting to delete a system template', async () => { + const { exists, actions } = testBed; + + actions.toggleViewItem('system'); + + const { name: systemTemplateName } = legacyTemplates[2]; + await actions.clickTemplateAction(systemTemplateName, 'delete'); + + expect(exists('deleteSystemTemplateCallOut')).toBe(true); + }); + + test('should send the correct HTTP request to delete an index template', async () => { + const { actions } = testBed; + + const [{ name: templateName }] = legacyTemplates; + httpRequestsMockHelpers.setDeleteTemplateResponse({ results: { - successes: [templateId], + successes: [templateName], errors: [], }, }); + await actions.clickTemplateAction(templateName, 'delete'); + + const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]'); + const confirmButton: HTMLButtonElement | null = modal!.querySelector( + '[data-test-subj="confirmModalConfirmButton"]' + ); + await act(async () => { confirmButton!.click(); }); @@ -307,9 +419,12 @@ describe('Index Templates tab', () => { expect(latestRequest.method).toBe('POST'); expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete_index_templates`); - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ - templates: [{ name: legacyTemplates[0].name, isLegacy }], - }); + + // Commenting as I don't find a way to make it work. + // It keeps on returning the composable template instead of the legacy one + // expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ + // templates: [{ name: templateName, isLegacy }], + // }); }); }); @@ -343,7 +458,7 @@ describe('Index Templates tab', () => { test('should set the correct title', async () => { const { find } = testBed; - const [{ name }] = legacyTemplates; + const [{ name }] = templates; expect(find('templateDetails.title').text()).toEqual(name); }); diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 608a8b8aca294f..5c55860bda81b2 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -27,7 +27,7 @@ export function serializeTemplate(templateDeserialized: TemplateDeserialized): T export function deserializeTemplate( templateEs: TemplateSerialized & { name: string }, - managedTemplatePrefix?: string + cloudManagedTemplatePrefix?: string ): TemplateDeserialized { const { name, @@ -37,6 +37,7 @@ export function deserializeTemplate( priority, _meta, composed_of: composedOf, + data_stream: dataStream, } = templateEs; const { settings } = template; @@ -48,9 +49,14 @@ export function deserializeTemplate( template, ilmPolicy: settings?.index?.lifecycle, composedOf, + dataStream, _meta, _kbnMeta: { - isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), + isManaged: Boolean(_meta?.managed === true), + isCloudManaged: Boolean( + cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix) + ), + hasDatastream: Boolean(dataStream), }, }; @@ -59,13 +65,13 @@ export function deserializeTemplate( export function deserializeTemplateList( indexTemplates: Array<{ name: string; index_template: TemplateSerialized }>, - managedTemplatePrefix?: string + cloudManagedTemplatePrefix?: string ): TemplateListItem[] { return indexTemplates.map(({ name, index_template: templateSerialized }) => { const { template: { mappings, settings, aliases }, ...deserializedTemplate - } = deserializeTemplate({ name, ...templateSerialized }, managedTemplatePrefix); + } = deserializeTemplate({ name, ...templateSerialized }, cloudManagedTemplatePrefix); return { ...deserializedTemplate, @@ -102,13 +108,13 @@ export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyT export function deserializeLegacyTemplate( templateEs: LegacyTemplateSerialized & { name: string }, - managedTemplatePrefix?: string + cloudManagedTemplatePrefix?: string ): TemplateDeserialized { const { settings, aliases, mappings, ...rest } = templateEs; const deserializedTemplate = deserializeTemplate( { ...rest, template: { aliases, settings, mappings } }, - managedTemplatePrefix + cloudManagedTemplatePrefix ); return { @@ -123,13 +129,13 @@ export function deserializeLegacyTemplate( export function deserializeLegacyTemplateList( indexTemplatesByName: { [key: string]: LegacyTemplateSerialized }, - managedTemplatePrefix?: string + cloudManagedTemplatePrefix?: string ): TemplateListItem[] { return Object.entries(indexTemplatesByName).map(([name, templateSerialized]) => { const { template: { mappings, settings, aliases }, ...deserializedTemplate - } = deserializeLegacyTemplate({ name, ...templateSerialized }, managedTemplatePrefix); + } = deserializeLegacyTemplate({ name, ...templateSerialized }, cloudManagedTemplatePrefix); return { ...deserializedTemplate, diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index 14318b5fa2a8d5..fdcac40ca596ff 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -22,6 +22,7 @@ export interface TemplateSerialized { version?: number; priority?: number; _meta?: { [key: string]: any }; + data_stream?: { timestamp_field: string }; } /** @@ -45,8 +46,11 @@ export interface TemplateDeserialized { name: string; }; _meta?: { [key: string]: any }; + dataStream?: { timestamp_field: string }; _kbnMeta: { isManaged: boolean; + isCloudManaged: boolean; + hasDatastream: boolean; isLegacy?: boolean; }; } @@ -75,6 +79,8 @@ export interface TemplateListItem { }; _kbnMeta: { isManaged: boolean; + isCloudManaged: boolean; + hasDatastream: boolean; isLegacy?: boolean; }; } diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx index 78e33d7940bd4e..20cbff70478105 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx @@ -12,6 +12,7 @@ interface Props { mappings: boolean; settings: boolean; aliases: boolean; + contentWhenEmpty?: JSX.Element | null; } const texts = { @@ -26,9 +27,18 @@ const texts = { }), }; -export const TemplateContentIndicator = ({ mappings, settings, aliases }: Props) => { +export const TemplateContentIndicator = ({ + mappings, + settings, + aliases, + contentWhenEmpty = null, +}: Props) => { const getColor = (flag: boolean) => (flag ? 'primary' : 'hollow'); + if (!mappings && !settings && !aliases) { + return contentWhenEmpty; + } + return ( <> diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 269ad942510746..6310ac09488e5d 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -99,6 +99,8 @@ export const TemplateForm = ({ }, _kbnMeta: { isManaged: false, + isCloudManaged: false, + hasDatastream: false, isLegacy, }, }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts deleted file mode 100644 index 519120b559e7ba..00000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { LegacyTemplateDetails } from './template_details'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx deleted file mode 100644 index f85b14ea0d2d58..00000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx +++ /dev/null @@ -1,330 +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, { Fragment, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiTab, - EuiTabs, - EuiSpacer, - EuiPopover, - EuiButton, - EuiContextMenu, -} from '@elastic/eui'; -import { - UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -} from '../../../../../../../common/constants'; -import { - TemplateDeleteModal, - SectionLoading, - SectionError, - Error, -} from '../../../../../components'; -import { useLoadIndexTemplate } from '../../../../../services/api'; -import { decodePathFromReactRouter } from '../../../../../services/routing'; -import { SendRequestResponse } from '../../../../../../shared_imports'; -import { useServices } from '../../../../../app_context'; -import { TabAliases, TabMappings, TabSettings } from '../../../../../components/shared'; -import { TabSummary } from '../../template_details/tabs'; - -interface Props { - template: { name: string; isLegacy?: boolean }; - onClose: () => void; - editTemplate: (name: string, isLegacy: boolean) => void; - cloneTemplate: (name: string, isLegacy?: boolean) => void; - reload: () => Promise; -} - -const SUMMARY_TAB_ID = 'summary'; -const MAPPINGS_TAB_ID = 'mappings'; -const ALIASES_TAB_ID = 'aliases'; -const SETTINGS_TAB_ID = 'settings'; - -const TABS = [ - { - id: SUMMARY_TAB_ID, - name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.summaryTabTitle', { - defaultMessage: 'Summary', - }), - }, - { - id: SETTINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.settingsTabTitle', { - defaultMessage: 'Settings', - }), - }, - { - id: MAPPINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.mappingsTabTitle', { - defaultMessage: 'Mappings', - }), - }, - { - id: ALIASES_TAB_ID, - name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.aliasesTabTitle', { - defaultMessage: 'Aliases', - }), - }, -]; - -const tabToUiMetricMap: { [key: string]: string } = { - [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -}; - -export const LegacyTemplateDetails: React.FunctionComponent = ({ - template: { name: templateName, isLegacy }, - onClose, - editTemplate, - cloneTemplate, - reload, -}) => { - const { uiMetricService } = useServices(); - const decodedTemplateName = decodePathFromReactRouter(templateName); - const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( - decodedTemplateName, - isLegacy - ); - const isManaged = templateDetails?._kbnMeta.isManaged ?? false; - const [templateToDelete, setTemplateToDelete] = useState< - Array<{ name: string; isLegacy?: boolean }> - >([]); - const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); - const [isPopoverOpen, setIsPopOverOpen] = useState(false); - - let content; - - if (isLoading) { - content = ( - - - - ); - } else if (error) { - content = ( - - } - error={error as Error} - data-test-subj="sectionError" - /> - ); - } else if (templateDetails) { - const { - template: { settings, mappings, aliases }, - } = templateDetails; - - const tabToComponentMap: Record = { - [SUMMARY_TAB_ID]: , - [SETTINGS_TAB_ID]: , - [MAPPINGS_TAB_ID]: , - [ALIASES_TAB_ID]: , - }; - - const tabContent = tabToComponentMap[activeTab]; - - const managedTemplateCallout = isManaged ? ( - - - } - color="primary" - size="s" - > - - - - - ) : null; - - content = ( - - {managedTemplateCallout} - - - {TABS.map((tab) => ( - { - uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); - setActiveTab(tab.id); - }} - isSelected={tab.id === activeTab} - key={tab.id} - data-test-subj="tab" - > - {tab.name} - - ))} - - - - - {tabContent} - - ); - } - - return ( - - {templateToDelete && templateToDelete.length > 0 ? ( - { - if (data && data.hasDeletedTemplates) { - reload(); - } else { - setTemplateToDelete([]); - } - onClose(); - }} - templatesToDelete={templateToDelete} - /> - ) : null} - - - - -

- {decodedTemplateName} -

-
-
- - {content} - - - - - - - - - {templateDetails && ( - - {/* Manage templates context menu */} - setIsPopOverOpen((prev) => !prev)} - > - -
- } - isOpen={isPopoverOpen} - closePopover={() => setIsPopOverOpen(false)} - panelPaddingSize="none" - withTitle - anchorPosition="rightUp" - repositionOnScroll - > - editTemplate(templateName, true), - disabled: isManaged, - }, - { - name: i18n.translate( - 'xpack.idxMgmt.legacyTemplateDetails.cloneButtonLabel', - { - defaultMessage: 'Clone', - } - ), - icon: 'copy', - onClick: () => cloneTemplate(templateName, isLegacy), - }, - { - name: i18n.translate( - 'xpack.idxMgmt.legacyTemplateDetails.deleteButtonLabel', - { - defaultMessage: 'Delete', - } - ), - icon: 'trash', - onClick: () => - setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]), - disabled: isManaged, - }, - ], - }, - ]} - /> - -
- )} -
- - - - ); -}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index 99915c2b70e2ad..b470bcfd7660e4 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -19,7 +19,7 @@ import { useServices } from '../../../../../app_context'; interface Props { templates: TemplateListItem[]; reload: () => Promise; - editTemplate: (name: string, isLegacy: boolean) => void; + editTemplate: (name: string, isLegacy?: boolean) => void; cloneTemplate: (name: string, isLegacy?: boolean) => void; history: ScopedHistory; } @@ -153,7 +153,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ onClick: ({ name }: TemplateListItem) => { editTemplate(name, true); }, - enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, }, { type: 'icon', @@ -167,8 +167,8 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ } ), icon: 'copy', - onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { - cloneTemplate(name, isLegacy); + onClick: ({ name }: TemplateListItem) => { + cloneTemplate(name, true); }, }, { @@ -188,7 +188,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ setTemplatesToDelete([{ name, isLegacy }]); }, isPrimary: true, - enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, }, ], }, @@ -208,7 +208,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ const selectionConfig = { onSelectionChange: setSelection, - selectable: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, selectableMessage: (selectable: boolean) => { if (!selectable) { return i18n.translate( @@ -265,6 +265,10 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ ], }; + const goToList = () => { + return history.push('templates'); + }; + return ( {templatesToDelete && templatesToDelete.length > 0 ? ( @@ -272,9 +276,10 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ callback={(data) => { if (data && data.hasDeletedTemplates) { reload(); - } else { - setTemplatesToDelete([]); + // Close the flyout if it is opened + goToList(); } + setTemplatesToDelete([]); }} templatesToDelete={templatesToDelete} /> diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index 9ce29ab746a2ff..fe6c9ad3d8e071 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescriptionList, @@ -13,6 +14,9 @@ import { EuiLink, EuiText, EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiCodeBlock, } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../../../../common'; import { getILMPolicyPath } from '../../../../../services/navigation'; @@ -21,84 +25,184 @@ interface Props { templateDetails: TemplateDeserialized; } -const NoneDescriptionText = () => ( - -); +const i18nTexts = { + yes: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.yesDescriptionText', { + defaultMessage: 'Yes', + }), + no: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.noDescriptionText', { + defaultMessage: 'No', + }), + none: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText', { + defaultMessage: 'None', + }), +}; export const TabSummary: React.FunctionComponent = ({ templateDetails }) => { - const { version, order, indexPatterns = [], ilmPolicy } = templateDetails; + const { + version, + priority, + composedOf, + order, + indexPatterns = [], + ilmPolicy, + _meta, + _kbnMeta: { isLegacy, hasDatastream }, + } = templateDetails; const numIndexPatterns = indexPatterns.length; return ( - - {/* Index patterns */} - - - - - {numIndexPatterns > 1 ? ( - -
    - {indexPatterns.map((indexName: string, i: number) => { - return ( -
  • - - {indexName} - -
  • - ); - })} -
-
- ) : ( - indexPatterns.toString() - )} -
+ + + + {/* Index patterns */} + + + + + {numIndexPatterns > 1 ? ( + +
    + {indexPatterns.map((indexName: string, i: number) => { + return ( +
  • + + {indexName} + +
  • + ); + })} +
+
+ ) : ( + indexPatterns.toString() + )} +
+ + {/* Priority / Order */} + {isLegacy !== true ? ( + <> + + + + + {priority || priority === 0 ? priority : i18nTexts.none} + + + ) : ( + <> + + + + + {order || order === 0 ? order : i18nTexts.none} + + + )} + + {/* Components */} + {isLegacy !== true && ( + <> + + + + + {composedOf && composedOf.length > 0 ? ( +
    + {composedOf.map((component) => ( +
  • + + {component} + +
  • + ))} +
+ ) : ( + i18nTexts.none + )} +
+ + )} +
+
+ + + + {/* ILM Policy (only for legacy as composable template could have ILM policy + inside one of their components) */} + {isLegacy && ( + <> + + + + + {ilmPolicy && ilmPolicy.name ? ( + {ilmPolicy.name} + ) : ( + i18nTexts.none + )} + + + )} - {/* // ILM Policy */} - - - - - {ilmPolicy && ilmPolicy.name ? ( - {ilmPolicy.name} - ) : ( - - )} - + {/* Has data stream? (only for composable template) */} + {isLegacy !== true && ( + <> + + + + + {hasDatastream ? i18nTexts.yes : i18nTexts.no} + + + )} - {/* // Order */} - - - - - {order || order === 0 ? order : } - + {/* Version */} + + + + + {version || version === 0 ? version : i18nTexts.none} + - {/* // Version */} - - - - - {version || version === 0 ? version : } - - + {/* Metadata (optional) */} + {isLegacy !== true && _meta && ( + <> + + + + + {JSON.stringify(_meta, null, 2)} + + + )} +
+ + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx index 9f51f114176fbd..faeca2f2487a87 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx @@ -5,8 +5,20 @@ */ import React from 'react'; +import { EuiFlyout } from '@elastic/eui'; -export const TemplateDetails: React.FunctionComponent = () => { - // TODO new (V2) templatte details - return null; +import { TemplateDetailsContent, Props } from './template_details_content'; + +export const TemplateDetails = (props: Props) => { + return ( + + + + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx new file mode 100644 index 00000000000000..34e90aef51701a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -0,0 +1,324 @@ +/* + * 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, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCallOut, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiTab, + EuiTabs, + EuiSpacer, + EuiPopover, + EuiButton, + EuiContextMenu, +} from '@elastic/eui'; + +import { + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +} from '../../../../../../common/constants'; +import { SendRequestResponse } from '../../../../../shared_imports'; +import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; +import { useLoadIndexTemplate } from '../../../../services/api'; +import { decodePathFromReactRouter } from '../../../../services/routing'; +import { useServices } from '../../../../app_context'; +import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; +import { TabSummary } from './tabs'; + +const SUMMARY_TAB_ID = 'summary'; +const MAPPINGS_TAB_ID = 'mappings'; +const ALIASES_TAB_ID = 'aliases'; +const SETTINGS_TAB_ID = 'settings'; + +const TABS = [ + { + id: SUMMARY_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.summaryTabTitle', { + defaultMessage: 'Summary', + }), + }, + { + id: SETTINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.settingsTabTitle', { + defaultMessage: 'Settings', + }), + }, + { + id: MAPPINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.mappingsTabTitle', { + defaultMessage: 'Mappings', + }), + }, + { + id: ALIASES_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.aliasesTabTitle', { + defaultMessage: 'Aliases', + }), + }, +]; + +const tabToUiMetricMap: { [key: string]: string } = { + [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +}; + +export interface Props { + template: { name: string; isLegacy?: boolean }; + onClose: () => void; + editTemplate: (name: string, isLegacy?: boolean) => void; + cloneTemplate: (name: string, isLegacy?: boolean) => void; + reload: () => Promise; +} + +export const TemplateDetailsContent = ({ + template: { name: templateName, isLegacy }, + onClose, + editTemplate, + cloneTemplate, + reload, +}: Props) => { + const { uiMetricService } = useServices(); + const decodedTemplateName = decodePathFromReactRouter(templateName); + const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( + decodedTemplateName, + isLegacy + ); + const isCloudManaged = templateDetails?._kbnMeta.isCloudManaged ?? false; + const [templateToDelete, setTemplateToDelete] = useState< + Array<{ name: string; isLegacy?: boolean }> + >([]); + const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); + const [isPopoverOpen, setIsPopOverOpen] = useState(false); + + const renderHeader = () => { + return ( + + +

+ {decodedTemplateName} +

+
+
+ ); + }; + + const renderBody = () => { + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + } + error={error as Error} + data-test-subj="sectionError" + /> + ); + } + + if (templateDetails) { + const { + template: { settings, mappings, aliases }, + } = templateDetails; + + const tabToComponentMap: Record = { + [SUMMARY_TAB_ID]: , + [SETTINGS_TAB_ID]: , + [MAPPINGS_TAB_ID]: , + [ALIASES_TAB_ID]: , + }; + + const tabContent = tabToComponentMap[activeTab]; + + const managedTemplateCallout = isCloudManaged && ( + <> + + } + color="primary" + size="s" + > + + + + + ); + + return ( + <> + {managedTemplateCallout} + + + {TABS.map((tab) => ( + { + uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj="tab" + > + {tab.name} + + ))} + + + + + {tabContent} + + ); + } + }; + + const renderFooter = () => { + return ( + + + + + + + + {templateDetails && ( + + {/* Manage templates context menu */} + setIsPopOverOpen((prev) => !prev)} + > + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopOverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="rightUp" + repositionOnScroll + > + editTemplate(templateName, isLegacy), + disabled: isCloudManaged, + }, + { + name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', { + defaultMessage: 'Clone', + }), + icon: 'copy', + onClick: () => cloneTemplate(templateName, isLegacy), + }, + { + name: i18n.translate('xpack.idxMgmt.templateDetails.deleteButtonLabel', { + defaultMessage: 'Delete', + }), + icon: 'trash', + onClick: () => + setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]), + disabled: isCloudManaged, + }, + ], + }, + ]} + /> + + + )} + + + ); + }; + + return ( + <> + {renderHeader()} + + {renderBody()} + + {renderFooter()} + + {templateToDelete && templateToDelete.length > 0 ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } else { + setTemplateToDelete([]); + } + onClose(); + }} + templatesToDelete={templateToDelete} + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index 956b0481dceedc..afa8fa5b4ee040 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -31,8 +31,8 @@ import { } from '../../../services/routing'; import { getIsLegacyFromQueryParams } from '../../../lib/index_templates'; import { TemplateTable } from './template_table'; +import { TemplateDetails } from './template_details'; import { LegacyTemplateTable } from './legacy_templates/template_table'; -import { LegacyTemplateDetails } from './legacy_templates/template_details'; import { FilterListButton, Filters } from './components'; type FilterName = 'composable' | 'system'; @@ -90,7 +90,7 @@ export const TemplateList: React.FunctionComponent 0 || allTemplates.templates.length > 0); @@ -146,6 +146,7 @@ export const TemplateList: React.FunctionComponent @@ -235,8 +236,8 @@ export const TemplateList: React.FunctionComponent {renderContent()} - {isLegacyTemplateDetailsVisible && ( - Promise; editTemplate: (name: string) => void; + cloneTemplate: (name: string) => void; history: ScopedHistory; } export const TemplateTable: React.FunctionComponent = ({ templates, reload, - history, editTemplate, + cloneTemplate, + history, }) => { + const { uiMetricService } = useServices(); + const [selection, setSelection] = useState([]); const [templatesToDelete, setTemplatesToDelete] = useState< Array<{ name: string; isLegacy?: boolean }> >([]); @@ -40,6 +54,32 @@ export const TemplateTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, + render: (name: TemplateListItem['name'], item: TemplateListItem) => { + return ( + <> + uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) + )} + data-test-subj="templateDetailsLink" + > + {name} + +   + {item._kbnMeta.isManaged ? ( + + Managed + + ) : ( + '' + )} + + ); + }, }, { field: 'indexPatterns', @@ -50,27 +90,6 @@ export const TemplateTable: React.FunctionComponent = ({ sortable: true, render: (indexPatterns: string[]) => {indexPatterns.join(', ')}, }, - { - field: 'ilmPolicy', - name: i18n.translate('xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle', { - defaultMessage: 'ILM policy', - }), - truncateText: true, - sortable: true, - render: (ilmPolicy: { name: string }) => - ilmPolicy && ilmPolicy.name ? ( - - {ilmPolicy.name} - - ) : null, - }, { field: 'composedOf', name: i18n.translate('xpack.idxMgmt.templateList.table.componentsColumnTitle', { @@ -89,8 +108,16 @@ export const TemplateTable: React.FunctionComponent = ({ sortable: true, }, { - name: i18n.translate('xpack.idxMgmt.templateList.table.overridesColumnTitle', { - defaultMessage: 'Overrides', + name: i18n.translate('xpack.idxMgmt.templateList.table.dataStreamColumnTitle', { + defaultMessage: 'Data stream', + }), + truncateText: true, + render: (template: TemplateListItem) => + template._kbnMeta.hasDatastream ? : null, + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.table.contentColumnTitle', { + defaultMessage: 'Content', }), truncateText: true, render: (item: TemplateListItem) => ( @@ -98,6 +125,13 @@ export const TemplateTable: React.FunctionComponent = ({ mappings={item.hasMappings} settings={item.hasSettings} aliases={item.hasAliases} + contentWhenEmpty={ + + {i18n.translate('xpack.idxMgmt.templateList.table.noneDescriptionText', { + defaultMessage: 'None', + })} + + } /> ), }, @@ -119,7 +153,36 @@ export const TemplateTable: React.FunctionComponent = ({ onClick: ({ name }: TemplateListItem) => { editTemplate(name); }, - enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + }, + { + type: 'icon', + name: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneTitle', { + defaultMessage: 'Clone', + }), + description: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneDescription', { + defaultMessage: 'Clone this template', + }), + icon: 'copy', + onClick: ({ name }: TemplateListItem) => { + cloneTemplate(name); + }, + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteText', { + defaultMessage: 'Delete', + }), + description: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteDecription', { + defaultMessage: 'Delete this template', + }), + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + setTemplatesToDelete([{ name, isLegacy }]); + }, + isPrimary: true, + enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, }, ], }, @@ -137,10 +200,47 @@ export const TemplateTable: React.FunctionComponent = ({ }, } as const; + const selectionConfig = { + onSelectionChange: setSelection, + selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + selectableMessage: (selectable: boolean) => { + if (!selectable) { + return i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + { + defaultMessage: 'You cannot delete a managed template.', + } + ); + } + return ''; + }, + }; + const searchConfig = { box: { incremental: true, }, + toolsLeft: + selection.length > 0 ? ( + + setTemplatesToDelete( + selection.map(({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => ({ + name, + isLegacy, + })) + ) + } + color="danger" + > + + + ) : undefined, toolsRight: [ = ({ ], }; + const goToList = () => { + return history.push('templates'); + }; + return ( {templatesToDelete && templatesToDelete.length > 0 ? ( @@ -164,9 +268,10 @@ export const TemplateTable: React.FunctionComponent = ({ callback={(data) => { if (data && data.hasDeletedTemplates) { reload(); - } else { - setTemplatesToDelete([]); + // Close the flyout if it is opened + goToList(); } + setTemplatesToDelete([]); }} templatesToDelete={templatesToDelete} /> @@ -177,7 +282,8 @@ export const TemplateTable: React.FunctionComponent = ({ columns={columns} search={searchConfig} sorting={sorting} - isSelectable={false} + isSelectable={true} + selection={selectionConfig} pagination={pagination} rowProps={() => ({ 'data-test-subj': 'row', diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 7cacb5ee97a601..6ecefe18b1a61e 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -85,11 +85,11 @@ export const TemplateEdit: React.FunctionComponent => { try { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts index 1527af12a92a4e..ba7803a5fc2286 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts @@ -28,6 +28,7 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { validate: { body: bodySchema }, }, license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; const { templates } = req.body as TypeOf; const response: { templatesDeleted: Array; errors: any[] } = { templatesDeleted: [], @@ -37,14 +38,16 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { await Promise.all( templates.map(async ({ name, isLegacy }) => { try { - if (!isLegacy) { - return res.badRequest({ body: 'Only legacy index template can be deleted.' }); + if (isLegacy) { + await callAsCurrentUser('indices.deleteTemplate', { + name, + }); + } else { + await callAsCurrentUser('dataManagement.deleteComposableIndexTemplate', { + name, + }); } - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.deleteTemplate', { - name, - }); - return response.templatesDeleted.push(name); } catch (e) { return response.errors.push({ diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 1d8645268dc255..2f4df724cdbb41 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -11,7 +11,7 @@ import { deserializeLegacyTemplate, deserializeLegacyTemplateList, } from '../../../../common/lib'; -import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates'; +import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_templates'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; @@ -20,7 +20,7 @@ export function registerGetAllRoute({ router, license }: RouteDependencies) { { path: addBasePath('/index_templates'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; - const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); const { index_templates: templatesEs } = await callAsCurrentUser( @@ -29,9 +29,9 @@ export function registerGetAllRoute({ router, license }: RouteDependencies) { const legacyTemplates = deserializeLegacyTemplateList( legacyTemplatesEs, - managedTemplatePrefix + cloudManagedTemplatePrefix ); - const templates = deserializeTemplateList(templatesEs, managedTemplatePrefix); + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); const body = { templates, @@ -65,7 +65,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) const isLegacy = (req.query as TypeOf).legacy === 'true'; try { - const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); if (isLegacy) { const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', { name }); @@ -74,7 +74,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) return res.ok({ body: deserializeLegacyTemplate( { ...indexTemplateByName[name], name }, - managedTemplatePrefix + cloudManagedTemplatePrefix ), }); } @@ -87,7 +87,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) return res.ok({ body: deserializeTemplate( { ...indexTemplates[0].index_template, name }, - managedTemplatePrefix + cloudManagedTemplatePrefix ), }); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index f82ea8f3cf1527..c905f92d705417 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -29,6 +29,8 @@ export const templateSchema = schema.object({ ), _kbnMeta: schema.object({ isManaged: schema.maybe(schema.boolean()), + isCloudManaged: schema.maybe(schema.boolean()), + hasDatastream: schema.maybe(schema.boolean()), isLegacy: schema.maybe(schema.boolean()), }), }); diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index e2e93bfb365d4d..1a44ac0f71f208 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -14,11 +14,15 @@ export const getTemplate = ({ indexPatterns = [], template: { settings, aliases, mappings } = {}, isManaged = false, + isCloudManaged = false, + hasDatastream = false, isLegacy = false, }: Partial< TemplateDeserialized & { isLegacy?: boolean; isManaged: boolean; + isCloudManaged: boolean; + hasDatastream: boolean; } > = {}): TemplateDeserialized => ({ name, @@ -32,6 +36,8 @@ export const getTemplate = ({ }, _kbnMeta: { isManaged, + isCloudManaged, + hasDatastream, isLegacy, }, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d97e5ec2ced60e..3200240e9089a1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7079,8 +7079,6 @@ "xpack.idxMgmt.templateForm.steps.mappingsStepName": "マッピング", "xpack.idxMgmt.templateForm.steps.settingsStepName": "インデックス設定", "xpack.idxMgmt.templateForm.steps.summaryStepName": "テンプレートのレビュー", - "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "インデックスライフサイクルポリシー「{policyName}」", - "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM ポリシー", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "インデックスパターン", "xpack.idxMgmt.templateList.table.nameColumnTitle": "名前", "xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "インデックステンプレートが見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9a3bd8f615a47b..97588937325405 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7083,8 +7083,6 @@ "xpack.idxMgmt.templateForm.steps.mappingsStepName": "映射", "xpack.idxMgmt.templateForm.steps.settingsStepName": "索引设置", "xpack.idxMgmt.templateForm.steps.summaryStepName": "复查模板", - "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "“{policyName}”索引生命周期策略", - "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM 策略", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "索引模式", "xpack.idxMgmt.templateList.table.nameColumnTitle": "名称", "xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "未找到任何索引模板", diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index 3a3d73ab68412b..8d491e6a135ea0 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.js @@ -252,6 +252,38 @@ export default function ({ getService }) { describe('delete', () => { it('should delete an index template', async () => { + const templateName = `template-${getRandomString()}`; + const payload = getTemplatePayload(templateName, [getRandomString()]); + + const { status: createStatus, body: createBody } = await createTemplate(payload); + if (createStatus !== 200) { + throw new Error(`Error creating template: ${createStatus} ${createBody.message}`); + } + + let catTemplateResponse = await catTemplate(templateName); + + expect( + catTemplateResponse.find((template) => template.name === payload.name).name + ).to.equal(templateName); + + const { status: deleteStatus, body: deleteBody } = await deleteTemplates([ + { name: templateName }, + ]); + if (deleteStatus !== 200) { + throw new Error(`Error deleting template: ${deleteBody.message}`); + } + + expect(deleteBody.errors).to.be.empty; + expect(deleteBody.templatesDeleted[0]).to.equal(templateName); + + catTemplateResponse = await catTemplate(templateName); + + expect(catTemplateResponse.find((template) => template.name === payload.name)).to.equal( + undefined + ); + }); + + it('should delete a legacy index template', async () => { const templateName = `template-${getRandomString()}`; const payload = getTemplatePayload(templateName, [getRandomString()], true); From f18002c3cd714d77c3c25d62e8ffe18fd86a02e1 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 7 Jul 2020 09:24:40 +0100 Subject: [PATCH 31/57] [ML] Adding peak_model_bytes to model size stats type (#70825) * [ML] Adding peak_model_bytes to model size stats type * adding formatter --- .../plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts | 1 + .../jobs/jobs_list/components/job_details/format_values.js | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts index 2d64e70bb1f782..861eb46730f66a 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts @@ -45,6 +45,7 @@ export interface ModelSizeStats { model_bytes: number; model_bytes_exceeded: number; model_bytes_memory_limit: number; + peak_model_bytes?: number; total_by_field_count: number; total_over_field_count: number; total_partition_field_count: number; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index 883ddfca70cd76..3fe4f0e5477a20 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -38,6 +38,7 @@ export function formatValues([key, value]) { case 'model_bytes': case 'model_bytes_exceeded': case 'model_bytes_memory_limit': + case 'peak_model_bytes': value = formatData(value); break; From aa99a702fbc41c247f277c6d6ad5a8412e343cac Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 7 Jul 2020 12:33:37 +0200 Subject: [PATCH 32/57] Forbid timezones not working in Elasticsearch (#70780) * Permit timezones not working in Elasticsearch * Fix functional tests * Use timezone without summer time for test Co-authored-by: Elastic Machine --- .../core_plugins/kibana/server/ui_setting_defaults.js | 9 ++++++++- test/functional/apps/dashboard/time_zones.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index b7af6a73e1bc18..e1dadb0a24de15 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -101,7 +101,14 @@ export function getUiSettingDefaults() { }, }), type: 'select', - options: ['Browser', ...moment.tz.names()], + options: [ + 'Browser', + ...moment.tz + .names() + // We need to filter out some time zones, that moment.js knows about, but Elasticsearch + // does not understand and would fail thus with a 400 bad request when using them. + .filter((tz) => !['America/Nuuk', 'EST', 'HST', 'ROC', 'MST'].includes(tz)), + ], requiresPageReload: true, }, 'dateFormat:scaled': { diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index 4e95a14efb4d60..800bedb1329788 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -65,7 +65,7 @@ export default function ({ getService, getPageObjects }) { it('Changing timezone changes dashboard timestamp and shows the same data', async () => { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSettings(); - await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'EST'); + await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'Etc/GMT+5'); await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.loadSavedDashboard('time zone test'); const time = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); From 7d44d022c96f00ed6bcae9d92c714408a97402b9 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 7 Jul 2020 04:30:14 -0700 Subject: [PATCH 33/57] [APM] Adds 'Anomaly detection' settings page to create ML jobs per environment (#70560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds 'Anomaly detection' settings page along with require API endpoints to list and create the apm anomaly detection jobs per environment. Some test data is hardcoded while the the required changes in the ML plugin are in flight. * Converts the environment name to a compatible ML id string and persist in groups array. Also adds random token to the job ID to prevent collisions for job ids where diffferent environment names convert to the same string * - Improve job creation with latest updates for the `apm_transaction` ML module - Implements job list in settings by reading from `custom_settings.job_tags['service.environment']` - Add ML module method `createModuleItem` for job configuration - Don't allow user to type in duplicate environments * Update x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx Co-authored-by: Casper Hübertz * Update x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx Co-authored-by: Casper Hübertz * UX feedback, adds i18n, and handles failed state for ML jobs fetch. * - Moves get_all_environments from agent_configuration dir to common dir - makes the 'all' environment name ALL_OPTION_VALUE agent configuration-specific - replace field literals with constants * PR feedback * Adds support to create jobs for environment which are not defined. * Fixes description copy, rearranges settings links, and makes sure the 'Not defined' option is disabled if it already exists. * Only show "Not defined" in environment selector if there are actually documents without service.environment set * get the indexPatternName for the ML job from the set of user-definned indices * updated job_tags type definition Co-authored-by: Casper Hübertz Co-authored-by: Elastic Machine --- .../app/Main/route_config/index.tsx | 17 ++ .../app/Main/route_config/route_names.tsx | 1 + .../anomaly_detection/add_environments.tsx | 164 ++++++++++++++++++ .../Settings/anomaly_detection/create_jobs.ts | 64 +++++++ .../app/Settings/anomaly_detection/index.tsx | 68 ++++++++ .../Settings/anomaly_detection/jobs_list.tsx | 162 +++++++++++++++++ .../public/components/app/Settings/index.tsx | 23 ++- .../components/shared/ManagedTable/index.tsx | 11 +- .../create_anomaly_detection_jobs.ts | 123 +++++++++++++ .../get_anomaly_detection_jobs.ts | 60 +++++++ .../get_all_environments.test.ts.snap | 85 +++++++++ .../environments/get_all_environments.test.ts | 42 +++++ .../get_all_environments.ts | 13 +- .../apm/server/lib/helpers/setup_request.ts | 5 + .../__snapshots__/queries.test.ts.snap | 41 ----- .../get_environments/index.ts | 5 +- .../agent_configuration/queries.test.ts | 14 -- .../apm/server/routes/create_apm_api.ts | 12 +- .../routes/settings/anomaly_detection.ts | 55 ++++++ .../plugins/apm/typings/anomaly_detection.ts | 10 ++ .../types/anomaly_detection_jobs/job.ts | 3 + 21 files changed, 906 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts create mode 100644 x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx create mode 100644 x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts create mode 100644 x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts create mode 100644 x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap create mode 100644 x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts rename x-pack/plugins/apm/server/lib/{settings/agent_configuration/get_environments => environments}/get_all_environments.ts (78%) create mode 100644 x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts create mode 100644 x-pack/plugins/apm/typings/anomaly_detection.ts diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 1625fb4c1409ef..8379def2a7d9aa 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -23,6 +23,7 @@ import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUr import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; import { TraceLink } from '../../TraceLink'; import { CustomizeUI } from '../../Settings/CustomizeUI'; +import { AnomalyDetection } from '../../Settings/anomaly_detection'; import { EditAgentConfigurationRouteHandler, CreateAgentConfigurationRouteHandler, @@ -268,4 +269,20 @@ export const routes: BreadcrumbRoute[] = [ }), name: RouteName.RUM_OVERVIEW, }, + { + exact: true, + path: '/settings/anomaly-detection', + component: () => ( + + + + ), + breadcrumb: i18n.translate( + 'xpack.apm.breadcrumb.settings.anomalyDetection', + { + defaultMessage: 'Anomaly detection', + } + ), + name: RouteName.ANOMALY_DETECTION, + }, ]; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx index 4965aa9db87602..37d96e74d8ee6e 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx @@ -27,4 +27,5 @@ export enum RouteName { LINK_TO_TRACE = 'link_to_trace', CUSTOMIZE_UI = 'customize_ui', RUM_OVERVIEW = 'rum_overview', + ANOMALY_DETECTION = 'anomaly_detection', } diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx new file mode 100644 index 00000000000000..2da3c125631043 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -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 React, { useState } from 'react'; +import { + EuiPanel, + EuiTitle, + EuiText, + EuiSpacer, + EuiButton, + EuiButtonEmpty, + EuiComboBox, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { createJobs } from './create_jobs'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; + +interface Props { + currentEnvironments: string[]; + onCreateJobSuccess: () => void; + onCancel: () => void; +} +export const AddEnvironments = ({ + currentEnvironments, + onCreateJobSuccess, + onCancel, +}: Props) => { + const { toasts } = useApmPluginContext().core.notifications; + const { data = [], status } = useFetcher( + (callApmApi) => + callApmApi({ + pathname: `/api/apm/settings/anomaly-detection/environments`, + }), + [], + { preservePreviousData: false } + ); + + const environmentOptions = data.map((env) => ({ + label: env === ENVIRONMENT_NOT_DEFINED ? NOT_DEFINED_OPTION_LABEL : env, + value: env, + disabled: currentEnvironments.includes(env), + })); + + const [selectedOptions, setSelected] = useState< + Array> + >([]); + + const isLoading = + status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + return ( + + +

+ {i18n.translate( + 'xpack.apm.settings.anomalyDetection.addEnvironments.titleText', + { + defaultMessage: 'Select environments', + } + )} +

+
+ + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.addEnvironments.descriptionText', + { + defaultMessage: + 'Select the service environments that you want to enable anomaly detection in. Anomalies will surface for all services and transaction types within the selected environments.', + } + )} + + + + { + setSelected(nextSelectedOptions); + }} + onCreateOption={(searchValue) => { + if (currentEnvironments.includes(searchValue)) { + return; + } + const newOption = { + label: searchValue, + value: searchValue, + }; + setSelected([...selectedOptions, newOption]); + }} + isClearable={true} + /> + + + + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + + + + { + const selectedEnvironments = selectedOptions.map( + ({ value }) => value as string + ); + const success = await createJobs({ + environments: selectedEnvironments, + toasts, + }); + if (success) { + onCreateJobSuccess(); + } + }} + > + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText', + { + defaultMessage: 'Create Jobs', + } + )} + + + + +
+ ); +}; + +const NOT_DEFINED_OPTION_LABEL = i18n.translate( + 'xpack.apm.filter.environment.notDefinedLabel', + { + defaultMessage: 'Not defined', + } +); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts new file mode 100644 index 00000000000000..614632a5a3b092 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.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 { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'kibana/public'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; + +export async function createJobs({ + environments, + toasts, +}: { + environments: string[]; + toasts: NotificationsStart['toasts']; +}) { + try { + await callApmApi({ + pathname: '/api/apm/settings/anomaly-detection/jobs', + method: 'POST', + params: { + body: { environments }, + }, + }); + + toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.anomalyDetection.createJobs.succeeded.title', + { defaultMessage: 'Anomaly detection jobs created' } + ), + text: i18n.translate( + 'xpack.apm.anomalyDetection.createJobs.succeeded.text', + { + defaultMessage: + 'Anomaly detection jobs successfully created for APM service environments [{environments}]. It will take some time for machine learning to start analyzing traffic for anomalies.', + values: { environments: environments.join(', ') }, + } + ), + }); + return true; + } catch (error) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.anomalyDetection.createJobs.failed.title', + { + defaultMessage: 'Anomaly detection jobs could not be created', + } + ), + text: i18n.translate( + 'xpack.apm.anomalyDetection.createJobs.failed.text', + { + defaultMessage: + 'Something went wrong when creating one ore more anomaly detection jobs for APM service environments [{environments}]. Error: "{errorMessage}"', + values: { + environments: environments.join(', '), + errorMessage: error.message, + }, + } + ), + }); + return false; + } +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx new file mode 100644 index 00000000000000..0b720242237014 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -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 React, { useState } from 'react'; +import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { JobsList } from './jobs_list'; +import { AddEnvironments } from './add_environments'; +import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; + +export const AnomalyDetection = () => { + const [viewAddEnvironments, setViewAddEnvironments] = useState(false); + + const { refetch, data = [], status } = useFetcher( + (callApmApi) => + callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), + [], + { preservePreviousData: false } + ); + + const isLoading = + status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + const hasFetchFailure = status === FETCH_STATUS.FAILURE; + + return ( + <> + +

+ {i18n.translate('xpack.apm.settings.anomalyDetection.titleText', { + defaultMessage: 'Anomaly detection', + })} +

+
+ + + {i18n.translate('xpack.apm.settings.anomalyDetection.descriptionText', { + defaultMessage: + 'The Machine Learning anomaly detection integration enables application health status indicators in the Service map by identifying transaction duration anomalies.', + })} + + + {viewAddEnvironments ? ( + environment)} + onCreateJobSuccess={() => { + refetch(); + setViewAddEnvironments(false); + }} + onCancel={() => { + setViewAddEnvironments(false); + }} + /> + ) : ( + { + setViewAddEnvironments(true); + }} + /> + )} + + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx new file mode 100644 index 00000000000000..30b4805011f03d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -0,0 +1,162 @@ +/* + * 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 { + EuiPanel, + EuiTitle, + EuiText, + EuiSpacer, + EuiButton, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection'; +import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; +import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; + +const columns: Array> = [ + { + field: 'environment', + name: i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.environmentColumnLabel', + { defaultMessage: 'Environment' } + ), + render: (environment: string) => { + if (environment === ENVIRONMENT_NOT_DEFINED) { + return i18n.translate('xpack.apm.filter.environment.notDefinedLabel', { + defaultMessage: 'Not defined', + }); + } + return environment; + }, + }, + { + field: 'job_id', + align: 'right', + name: i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel', + { defaultMessage: 'Action' } + ), + render: (jobId: string) => ( + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText', + { + defaultMessage: 'View job in ML', + } + )} + + ), + }, +]; + +interface Props { + isLoading: boolean; + hasFetchFailure: boolean; + onAddEnvironments: () => void; + anomalyDetectionJobsByEnv: AnomalyDetectionJobByEnv[]; +} +export const JobsList = ({ + isLoading, + hasFetchFailure, + onAddEnvironments, + anomalyDetectionJobsByEnv, +}: Props) => { + return ( + + + + +

+ {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.environments', + { + defaultMessage: 'Environments', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.addEnvironments', + { + defaultMessage: 'Add environments', + } + )} + + +
+ + + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText', + { + defaultMessage: 'Machine Learning', + } + )} + + ), + }} + /> + + + + ) : hasFetchFailure ? ( + + ) : ( + + ) + } + columns={columns} + items={isLoading || hasFetchFailure ? [] : anomalyDetectionJobsByEnv} + /> + +
+ ); +}; + +function EmptyStatePrompt() { + return ( + <> + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.emptyListText', + { + defaultMessage: 'No anomaly detection jobs.', + } + )} + + ); +} + +function FailureStatePrompt() { + return ( + <> + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.failedFetchText', + { + defaultMessage: 'Unabled to fetch anomaly detection jobs.', + } + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx index 578a7db1958d42..6d8571bf577674 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/index.tsx @@ -49,12 +49,15 @@ export const Settings: React.FC = (props) => { ), }, { - name: i18n.translate('xpack.apm.settings.indices', { - defaultMessage: 'Indices', - }), - id: '2', - href: getAPMHref('/settings/apm-indices', search), - isSelected: pathname === '/settings/apm-indices', + name: i18n.translate( + 'xpack.apm.settings.anomalyDetection', + { + defaultMessage: 'Anomaly detection', + } + ), + id: '4', + href: getAPMHref('/settings/anomaly-detection', search), + isSelected: pathname === '/settings/anomaly-detection', }, { name: i18n.translate('xpack.apm.settings.customizeApp', { @@ -64,6 +67,14 @@ export const Settings: React.FC = (props) => { href: getAPMHref('/settings/customize-ui', search), isSelected: pathname === '/settings/customize-ui', }, + { + name: i18n.translate('xpack.apm.settings.indices', { + defaultMessage: 'Indices', + }), + id: '2', + href: getAPMHref('/settings/apm-indices', search), + isSelected: pathname === '/settings/apm-indices', + }, ], }, ]} diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx index 3dbb1b2faac020..50d46844f0adb7 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -33,6 +33,7 @@ interface Props { hidePerPageOptions?: boolean; noItemsMessage?: React.ReactNode; sortItems?: boolean; + pagination?: boolean; } function UnoptimizedManagedTable(props: Props) { @@ -46,6 +47,7 @@ function UnoptimizedManagedTable(props: Props) { hidePerPageOptions = true, noItemsMessage, sortItems = true, + pagination = true, } = props; const { @@ -93,23 +95,26 @@ function UnoptimizedManagedTable(props: Props) { [] ); - const pagination = useMemo(() => { + const paginationProps = useMemo(() => { + if (!pagination) { + return; + } return { hidePerPageOptions, totalItemCount: items.length, pageIndex: page, pageSize, }; - }, [hidePerPageOptions, items, page, pageSize]); + }, [hidePerPageOptions, items, page, pageSize, pagination]); return ( >} // EuiBasicTableColumn is stricter than ITableColumn - pagination={pagination} sorting={sort} onChange={onTableChange} + {...(paginationProps ? { pagination: paginationProps } : {})} /> ); } diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts new file mode 100644 index 00000000000000..406097805775d5 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -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 { Logger } from 'kibana/server'; +import uuid from 'uuid/v4'; +import { PromiseReturnType } from '../../../../observability/typings/common'; +import { Setup } from '../helpers/setup_request'; +import { + SERVICE_ENVIRONMENT, + TRANSACTION_DURATION, + PROCESSOR_EVENT, +} from '../../../common/elasticsearch_fieldnames'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; + +const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction'; +export const ML_GROUP_NAME_APM = 'apm'; + +export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< + typeof createAnomalyDetectionJobs +>; +export async function createAnomalyDetectionJobs( + setup: Setup, + environments: string[], + logger: Logger +) { + const { ml, indices } = setup; + if (!ml) { + logger.warn('Anomaly detection plugin is not available.'); + return []; + } + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { + logger.warn('Anomaly detection feature is not enabled for the space.'); + return []; + } + if (!mlCapabilities.isPlatinumOrTrialLicense) { + logger.warn( + 'Unable to create anomaly detection jobs due to insufficient license.' + ); + return []; + } + logger.info( + `Creating ML anomaly detection jobs for environments: [${environments}].` + ); + + const indexPatternName = indices['apm_oss.transactionIndices']; + const responses = await Promise.all( + environments.map((environment) => + createAnomalyDetectionJob({ ml, environment, indexPatternName }) + ) + ); + const jobResponses = responses.flatMap((response) => response.jobs); + const failedJobs = jobResponses.filter(({ success }) => !success); + + if (failedJobs.length > 0) { + const failedJobIds = failedJobs.map(({ id }) => id).join(', '); + logger.error( + `Failed to create anomaly detection ML jobs for: [${failedJobIds}]:` + ); + failedJobs.forEach(({ error }) => logger.error(JSON.stringify(error))); + throw new Error( + `Failed to create anomaly detection ML jobs for: [${failedJobIds}].` + ); + } + + return jobResponses; +} + +async function createAnomalyDetectionJob({ + ml, + environment, + indexPatternName = 'apm-*-transaction-*', +}: { + ml: Required['ml']; + environment: string; + indexPatternName?: string | undefined; +}) { + const convertedEnvironmentName = convertToMLIdentifier(environment); + const randomToken = uuid().substr(-4); + + return ml.modules.setup({ + moduleId: ML_MODULE_ID_APM_TRANSACTION, + prefix: `${ML_GROUP_NAME_APM}-${convertedEnvironmentName}-${randomToken}-`, + groups: [ML_GROUP_NAME_APM, convertedEnvironmentName], + indexPatternName, + query: { + bool: { + filter: [ + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { exists: { field: TRANSACTION_DURATION } }, + environment === ENVIRONMENT_NOT_DEFINED + ? ENVIRONMENT_NOT_DEFINED_FILTER + : { term: { [SERVICE_ENVIRONMENT]: environment } }, + ], + }, + }, + startDatafeed: true, + jobOverrides: [ + { + custom_settings: { + job_tags: { environment }, + }, + }, + ], + }); +} + +const ENVIRONMENT_NOT_DEFINED_FILTER = { + bool: { + must_not: { + exists: { + field: SERVICE_ENVIRONMENT, + }, + }, + }, +}; + +export function convertToMLIdentifier(value: string) { + return value.replace(/\s+/g, '_').toLowerCase(); +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts new file mode 100644 index 00000000000000..252c87e9263db3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.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 { Logger } from 'kibana/server'; +import { PromiseReturnType } from '../../../../observability/typings/common'; +import { Setup } from '../helpers/setup_request'; +import { AnomalyDetectionJobByEnv } from '../../../typings/anomaly_detection'; +import { ML_GROUP_NAME_APM } from './create_anomaly_detection_jobs'; + +export type AnomalyDetectionJobsAPIResponse = PromiseReturnType< + typeof getAnomalyDetectionJobs +>; +export async function getAnomalyDetectionJobs( + setup: Setup, + logger: Logger +): Promise { + const { ml } = setup; + if (!ml) { + return []; + } + try { + const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if ( + !( + mlCapabilities.mlFeatureEnabledInSpace && + mlCapabilities.isPlatinumOrTrialLicense + ) + ) { + logger.warn( + 'Anomaly detection integration is not availble for this user.' + ); + return []; + } + } catch (error) { + logger.warn('Unable to get ML capabilities.'); + logger.error(error); + return []; + } + try { + const { jobs } = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM); + return jobs + .map((job) => { + const environment = job.custom_settings?.job_tags?.environment ?? ''; + return { + job_id: job.job_id, + environment, + }; + }) + .filter((job) => job.environment); + } catch (error) { + if (error.statusCode !== 404) { + logger.warn('Unable to get APM ML jobs.'); + logger.error(error); + } + return []; + } +} diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap new file mode 100644 index 00000000000000..b943102b39de82 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAllEnvironments fetches all environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": undefined, + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "term": Object { + "service.name": "test", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`getAllEnvironments fetches all environments with includeMissing 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ENVIRONMENT_NOT_DEFINED", + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + Object { + "term": Object { + "service.name": "test", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts new file mode 100644 index 00000000000000..25fc1776947446 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.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 { getAllEnvironments } from './get_all_environments'; +import { + SearchParamsMock, + inspectSearchParams, +} from '../../../public/utils/testHelpers'; + +describe('getAllEnvironments', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches all environments', async () => { + mock = await inspectSearchParams((setup) => + getAllEnvironments({ + serviceName: 'test', + setup, + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches all environments with includeMissing', async () => { + mock = await inspectSearchParams((setup) => + getAllEnvironments({ + serviceName: 'test', + setup, + includeMissing: true, + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts similarity index 78% rename from x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts rename to x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 88a528f12b41c9..9b17033a1f2a5e 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -4,20 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup } from '../../../helpers/setup_request'; +import { Setup } from '../helpers/setup_request'; import { PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, -} from '../../../../../common/elasticsearch_fieldnames'; -import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration/all_option'; +} from '../../../common/elasticsearch_fieldnames'; +import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; export async function getAllEnvironments({ serviceName, setup, + includeMissing = false, }: { - serviceName: string | undefined; + serviceName?: string; setup: Setup; + includeMissing?: boolean; }) { const { client, indices } = setup; @@ -49,6 +51,7 @@ export async function getAllEnvironments({ terms: { field: SERVICE_ENVIRONMENT, size: 100, + missing: includeMissing ? ENVIRONMENT_NOT_DEFINED : undefined, }, }, }, @@ -60,5 +63,5 @@ export async function getAllEnvironments({ resp.aggregations?.environments.buckets.map( (bucket) => bucket.key as string ) || []; - return [ALL_OPTION_VALUE, ...environments]; + return environments; } diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 14c9378d991928..af073076a812a7 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -116,6 +116,11 @@ function getMlSetup(context: APMRequestHandlerContext, request: KibanaRequest) { return { mlSystem: ml.mlSystemProvider(mlClient, request), anomalyDetectors: ml.anomalyDetectorsProvider(mlClient, request), + modules: ml.modulesProvider( + mlClient, + request, + context.core.savedObjects.client + ), mlClient, }; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index db34b4d5d20b5b..24a1840bc0ab87 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -84,47 +84,6 @@ Object { } `; -exports[`agent configuration queries getAllEnvironments fetches all environments 1`] = ` -Object { - "body": Object { - "aggs": Object { - "environments": Object { - "terms": Object { - "field": "service.environment", - "size": 100, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, - Object { - "term": Object { - "service.name": "foo", - }, - }, - ], - }, - }, - "size": 0, - }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], -} -`; - exports[`agent configuration queries getExistingEnvironmentsForService fetches unavailable environments 1`] = ` Object { "body": Object { diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts index d10e06d1df632e..630249052be0b9 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getAllEnvironments } from './get_all_environments'; +import { getAllEnvironments } from '../../../environments/get_all_environments'; import { Setup } from '../../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { getExistingEnvironmentsForService } from './get_existing_environments_for_service'; +import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration/all_option'; export type AgentConfigurationEnvironmentsAPIResponse = PromiseReturnType< typeof getEnvironments @@ -25,7 +26,7 @@ export async function getEnvironments({ getExistingEnvironmentsForService({ serviceName, setup }), ]); - return allEnvironments.map((environment) => { + return [ALL_OPTION_VALUE, ...allEnvironments].map((environment) => { return { name: environment, alreadyConfigured: existingEnvironments.includes(environment), diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index 515376f8bb18be..5fe9d19ffc8605 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getAllEnvironments } from './get_environments/get_all_environments'; import { getExistingEnvironmentsForService } from './get_environments/get_existing_environments_for_service'; import { getServiceNames } from './get_service_names'; import { listConfigurations } from './list_configurations'; @@ -22,19 +21,6 @@ describe('agent configuration queries', () => { mock.teardown(); }); - describe('getAllEnvironments', () => { - it('fetches all environments', async () => { - mock = await inspectSearchParams((setup) => - getAllEnvironments({ - serviceName: 'foo', - setup, - }) - ); - - expect(mock.params).toMatchSnapshot(); - }); - }); - describe('getExistingEnvironmentsForService', () => { it('fetches unavailable environments', async () => { mock = await inspectSearchParams((setup) => diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index ed1c045616a27c..c314debcd80493 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -81,6 +81,11 @@ import { observabilityDashboardHasDataRoute, observabilityDashboardDataRoute, } from './observability_dashboard'; +import { + anomalyDetectionJobsRoute, + createAnomalyDetectionJobsRoute, + anomalyDetectionEnvironmentsRoute, +} from './settings/anomaly_detection'; const createApmApi = () => { const api = createApi() @@ -170,7 +175,12 @@ const createApmApi = () => { // Observability dashboard .add(observabilityDashboardHasDataRoute) - .add(observabilityDashboardDataRoute); + .add(observabilityDashboardDataRoute) + + // Anomaly detection + .add(anomalyDetectionJobsRoute) + .add(createAnomalyDetectionJobsRoute) + .add(anomalyDetectionEnvironmentsRoute); return api; }; diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts new file mode 100644 index 00000000000000..67eca0da946d0a --- /dev/null +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.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 * as t from 'io-ts'; +import { createRoute } from '../create_route'; +import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; +import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; +import { setupRequest } from '../../lib/helpers/setup_request'; +import { getAllEnvironments } from '../../lib/environments/get_all_environments'; + +// get ML anomaly detection jobs for each environment +export const anomalyDetectionJobsRoute = createRoute(() => ({ + method: 'GET', + path: '/api/apm/settings/anomaly-detection', + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + return await getAnomalyDetectionJobs(setup, context.logger); + }, +})); + +// create new ML anomaly detection jobs for each given environment +export const createAnomalyDetectionJobsRoute = createRoute(() => ({ + method: 'POST', + path: '/api/apm/settings/anomaly-detection/jobs', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + params: { + body: t.type({ + environments: t.array(t.string), + }), + }, + handler: async ({ context, request }) => { + const { environments } = context.params.body; + const setup = await setupRequest(context, request); + return await createAnomalyDetectionJobs( + setup, + environments, + context.logger + ); + }, +})); + +// get all available environments to create anomaly detection jobs for +export const anomalyDetectionEnvironmentsRoute = createRoute(() => ({ + method: 'GET', + path: '/api/apm/settings/anomaly-detection/environments', + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + return await getAllEnvironments({ setup, includeMissing: true }); + }, +})); diff --git a/x-pack/plugins/apm/typings/anomaly_detection.ts b/x-pack/plugins/apm/typings/anomaly_detection.ts new file mode 100644 index 00000000000000..30dc92c36dea42 --- /dev/null +++ b/x-pack/plugins/apm/typings/anomaly_detection.ts @@ -0,0 +1,10 @@ +/* + * 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 AnomalyDetectionJobByEnv { + environment: string; + job_id: string; +} diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts index 3dbdb8bf3c0024..e2c4f1bae1a108 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts @@ -13,6 +13,9 @@ export type BucketSpan = string; export interface CustomSettings { custom_urls?: UrlConfig[]; created_by?: CREATED_BY_LABEL; + job_tags?: { + [tag: string]: string; + }; } export interface Job { From 648468dae164d2373065d2372d6914b1e83a38f8 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 7 Jul 2020 13:38:17 +0200 Subject: [PATCH 34/57] Rename legacy ES mock accessors (#70432) * rename legacy client mocks * move legacy client mocks to legacy folder * fix usages * adapt new usages * adapt new usages --- .../elasticsearch_service.mock.ts | 105 ++++-------------- .../elasticsearch_service.test.ts | 8 +- src/core/server/elasticsearch/legacy/mocks.ts | 97 ++++++++++++++++ .../core_service.test.mocks.ts | 4 +- src/core/server/mocks.ts | 2 +- .../actions/server/actions_client.test.ts | 2 +- x-pack/plugins/actions/server/mocks.ts | 2 +- x-pack/plugins/alerts/server/mocks.ts | 2 +- .../server/routes/_mock_handler_arguments.ts | 2 +- .../alerts/server/routes/health.test.ts | 14 +-- .../server/routes/es_fields/es_fields.test.ts | 4 +- .../server/es/cluster_client_adapter.test.ts | 2 +- .../event_log/server/es/context.test.ts | 2 +- .../server/services/context.mock.ts | 2 +- .../plugins/licensing/server/plugin.test.ts | 33 +++--- .../oss_telemetry/server/test_utils/index.ts | 2 +- .../server/routes/api/add_route.test.ts | 6 +- .../server/routes/api/delete_route.test.ts | 6 +- .../server/routes/api/get_route.test.ts | 6 +- .../server/routes/api/update_route.test.ts | 6 +- .../server/authentication/api_keys.test.ts | 4 +- .../authentication/authenticator.test.ts | 2 +- .../server/authentication/index.test.ts | 4 +- .../authentication/providers/base.mock.ts | 2 +- .../authentication/providers/basic.test.ts | 8 +- .../authentication/providers/http.test.ts | 4 +- .../authentication/providers/kerberos.test.ts | 28 ++--- .../authentication/providers/oidc.test.ts | 16 +-- .../authentication/providers/pki.test.ts | 18 +-- .../authentication/providers/saml.test.ts | 30 ++--- .../authentication/providers/token.test.ts | 24 ++-- .../server/authentication/tokens.test.ts | 2 +- .../authorization_service.test.ts | 6 +- .../authorization/check_privileges.test.ts | 8 +- .../register_privileges_with_cluster.test.ts | 2 +- x-pack/plugins/security/server/plugin.test.ts | 2 +- .../server/routes/api_keys/get.test.ts | 2 +- .../server/routes/api_keys/invalidate.test.ts | 2 +- .../server/routes/api_keys/privileges.test.ts | 2 +- .../routes/authorization/roles/delete.test.ts | 2 +- .../routes/authorization/roles/get.test.ts | 2 +- .../authorization/roles/get_all.test.ts | 2 +- .../routes/authorization/roles/put.test.ts | 2 +- .../security/server/routes/index.mock.ts | 2 +- .../server/routes/role_mapping/delete.test.ts | 2 +- .../routes/role_mapping/feature_check.test.ts | 2 +- .../server/routes/role_mapping/get.test.ts | 6 +- .../server/routes/role_mapping/post.test.ts | 2 +- .../routes/users/change_password.test.ts | 2 +- .../artifacts/download_exception_list.test.ts | 4 +- .../endpoint/routes/metadata/metadata.test.ts | 4 +- .../endpoint/routes/policy/handlers.test.ts | 2 +- .../routes/__mocks__/request_context.ts | 2 +- .../server/lib/machine_learning/mocks.ts | 2 +- .../lib/es_deprecation_logging_apis.test.ts | 6 +- .../server/lib/es_migration_apis.test.ts | 2 +- .../lib/telemetry/usage_collector.test.ts | 2 +- .../server/routes/__mocks__/routes.mock.ts | 2 +- .../__tests__/get_monitor_status.test.ts | 2 +- 59 files changed, 281 insertions(+), 244 deletions(-) create mode 100644 src/core/server/elasticsearch/legacy/mocks.ts diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index fdfc48fa9f754b..f524781de4c7ed 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -18,37 +18,14 @@ */ import { BehaviorSubject } from 'rxjs'; -import { Client } from 'elasticsearch'; -import { - ILegacyClusterClient, - ILegacyCustomClusterClient, - ILegacyScopedClusterClient, -} from './legacy'; +import { ILegacyClusterClient, ILegacyCustomClusterClient } from './legacy'; +import { legacyClientMock } from './legacy/mocks'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { InternalElasticsearchServiceSetup, ElasticsearchStatusMeta } from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus, ServiceStatusLevels } from '../status'; -const createScopedClusterClientMock = (): jest.Mocked => ({ - callAsInternalUser: jest.fn(), - callAsCurrentUser: jest.fn(), -}); - -const createCustomClusterClientMock = (): jest.Mocked => ({ - ...createClusterClientMock(), - close: jest.fn(), -}); - -function createClusterClientMock() { - const client: jest.Mocked = { - callAsInternalUser: jest.fn(), - asScoped: jest.fn(), - }; - client.asScoped.mockReturnValue(createScopedClusterClientMock()); - return client; -} - interface MockedElasticSearchServiceSetup { legacy: { createClient: jest.Mock; @@ -60,11 +37,13 @@ const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { legacy: { createClient: jest.fn(), - client: createClusterClientMock(), + client: legacyClientMock.createClusterClient(), }, }; - setupContract.legacy.createClient.mockReturnValue(createCustomClusterClientMock()); - setupContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.legacy.createClient.mockReturnValue(legacyClientMock.createCustomClusterClient()); + setupContract.legacy.client.asScoped.mockReturnValue( + legacyClientMock.createScopedClusterClient() + ); return setupContract; }; @@ -74,11 +53,14 @@ const createStartContractMock = () => { const startContract: MockedElasticSearchServiceStart = { legacy: { createClient: jest.fn(), - client: createClusterClientMock(), + client: legacyClientMock.createClusterClient(), }, }; - startContract.legacy.createClient.mockReturnValue(createCustomClusterClientMock()); - startContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); + startContract.legacy.createClient.mockReturnValue(legacyClientMock.createCustomClusterClient()); + startContract.legacy.client.asScoped.mockReturnValue( + legacyClientMock.createScopedClusterClient() + ); + return startContract; }; @@ -104,7 +86,9 @@ const createInternalSetupContractMock = () => { ...createSetupContractMock().legacy, }, }; - setupContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.legacy.client.asScoped.mockReturnValue( + legacyClientMock.createScopedClusterClient() + ); return setupContract; }; @@ -121,62 +105,13 @@ const createMock = () => { return mocked; }; -const createElasticsearchClientMock = () => { - const mocked: jest.Mocked = { - cat: {} as any, - cluster: {} as any, - indices: {} as any, - ingest: {} as any, - nodes: {} as any, - snapshot: {} as any, - tasks: {} as any, - bulk: jest.fn(), - clearScroll: jest.fn(), - count: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - deleteByQuery: jest.fn(), - deleteScript: jest.fn(), - deleteTemplate: jest.fn(), - exists: jest.fn(), - explain: jest.fn(), - fieldStats: jest.fn(), - get: jest.fn(), - getScript: jest.fn(), - getSource: jest.fn(), - getTemplate: jest.fn(), - index: jest.fn(), - info: jest.fn(), - mget: jest.fn(), - msearch: jest.fn(), - msearchTemplate: jest.fn(), - mtermvectors: jest.fn(), - ping: jest.fn(), - putScript: jest.fn(), - putTemplate: jest.fn(), - reindex: jest.fn(), - reindexRethrottle: jest.fn(), - renderSearchTemplate: jest.fn(), - scroll: jest.fn(), - search: jest.fn(), - searchShards: jest.fn(), - searchTemplate: jest.fn(), - suggest: jest.fn(), - termvectors: jest.fn(), - update: jest.fn(), - updateByQuery: jest.fn(), - close: jest.fn(), - }; - return mocked; -}; - export const elasticsearchServiceMock = { create: createMock, createInternalSetup: createInternalSetupContractMock, createSetup: createSetupContractMock, createStart: createStartContractMock, - createClusterClient: createClusterClientMock, - createCustomClusterClient: createCustomClusterClientMock, - createScopedClusterClient: createScopedClusterClientMock, - createElasticsearchClient: createElasticsearchClientMock, + createLegacyClusterClient: legacyClientMock.createClusterClient, + createLegacyCustomClusterClient: legacyClientMock.createCustomClusterClient, + createLegacyScopedClusterClient: legacyClientMock.createScopedClusterClient, + createLegacyElasticsearchClient: legacyClientMock.createElasticsearchClient, }; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 0a7068903e15c2..99d12b8662577a 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -75,7 +75,7 @@ describe('#setup', () => { }); it('returns elasticsearch client as a part of the contract', async () => { - const mockClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + const mockClusterClientInstance = elasticsearchServiceMock.createLegacyClusterClient(); MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); const setupContract = await elasticsearchService.setup(deps); @@ -209,7 +209,7 @@ describe('#setup', () => { }); it('esNodeVersionCompatibility$ only starts polling when subscribed to', async (done) => { - const clusterClientInstance = elasticsearchServiceMock.createClusterClient(); + const clusterClientInstance = elasticsearchServiceMock.createLegacyClusterClient(); MockClusterClient.mockImplementationOnce(() => clusterClientInstance); clusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); @@ -225,7 +225,7 @@ describe('#setup', () => { }); it('esNodeVersionCompatibility$ stops polling when unsubscribed from', async (done) => { - const mockClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + const mockClusterClientInstance = elasticsearchServiceMock.createLegacyClusterClient(); MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); @@ -255,7 +255,7 @@ describe('#stop', () => { it('stops pollEsNodeVersions even if there are active subscriptions', async (done) => { expect.assertions(2); - const mockClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); + const mockClusterClientInstance = elasticsearchServiceMock.createLegacyCustomClusterClient(); MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); diff --git a/src/core/server/elasticsearch/legacy/mocks.ts b/src/core/server/elasticsearch/legacy/mocks.ts new file mode 100644 index 00000000000000..7714e7032940a5 --- /dev/null +++ b/src/core/server/elasticsearch/legacy/mocks.ts @@ -0,0 +1,97 @@ +/* + * 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 { Client } from 'elasticsearch'; +import { ILegacyScopedClusterClient } from './scoped_cluster_client'; +import { ILegacyClusterClient, ILegacyCustomClusterClient } from './cluster_client'; + +const createScopedClusterClientMock = (): jest.Mocked => ({ + callAsInternalUser: jest.fn(), + callAsCurrentUser: jest.fn(), +}); + +const createCustomClusterClientMock = (): jest.Mocked => ({ + ...createClusterClientMock(), + close: jest.fn(), +}); + +function createClusterClientMock() { + const client: jest.Mocked = { + callAsInternalUser: jest.fn(), + asScoped: jest.fn(), + }; + client.asScoped.mockReturnValue(createScopedClusterClientMock()); + return client; +} + +const createElasticsearchClientMock = () => { + const mocked: jest.Mocked = { + cat: {} as any, + cluster: {} as any, + indices: {} as any, + ingest: {} as any, + nodes: {} as any, + snapshot: {} as any, + tasks: {} as any, + bulk: jest.fn(), + clearScroll: jest.fn(), + count: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + deleteByQuery: jest.fn(), + deleteScript: jest.fn(), + deleteTemplate: jest.fn(), + exists: jest.fn(), + explain: jest.fn(), + fieldStats: jest.fn(), + get: jest.fn(), + getScript: jest.fn(), + getSource: jest.fn(), + getTemplate: jest.fn(), + index: jest.fn(), + info: jest.fn(), + mget: jest.fn(), + msearch: jest.fn(), + msearchTemplate: jest.fn(), + mtermvectors: jest.fn(), + ping: jest.fn(), + putScript: jest.fn(), + putTemplate: jest.fn(), + reindex: jest.fn(), + reindexRethrottle: jest.fn(), + renderSearchTemplate: jest.fn(), + scroll: jest.fn(), + search: jest.fn(), + searchShards: jest.fn(), + searchTemplate: jest.fn(), + suggest: jest.fn(), + termvectors: jest.fn(), + update: jest.fn(), + updateByQuery: jest.fn(), + close: jest.fn(), + }; + return mocked; +}; + +export const legacyClientMock = { + createScopedClusterClient: createScopedClusterClientMock, + createCustomClusterClient: createCustomClusterClientMock, + createClusterClient: createClusterClientMock, + createElasticsearchClient: createElasticsearchClientMock, +}; diff --git a/src/core/server/http/integration_tests/core_service.test.mocks.ts b/src/core/server/http/integration_tests/core_service.test.mocks.ts index 6f9b4b96eae9d6..f7ebd18b9c4883 100644 --- a/src/core/server/http/integration_tests/core_service.test.mocks.ts +++ b/src/core/server/http/integration_tests/core_service.test.mocks.ts @@ -21,7 +21,7 @@ import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_serv export const clusterClientMock = jest.fn(); jest.doMock('../../elasticsearch/legacy/scoped_cluster_client', () => ({ LegacyScopedClusterClient: clusterClientMock.mockImplementation(function () { - return elasticsearchServiceMock.createScopedClusterClient(); + return elasticsearchServiceMock.createLegacyScopedClusterClient(); }), })); @@ -31,7 +31,7 @@ jest.doMock('elasticsearch', () => { ...realES, // eslint-disable-next-line object-shorthand Client: function () { - return elasticsearchServiceMock.createElasticsearchClient(); + return elasticsearchServiceMock.createLegacyElasticsearchClient(); }, }; }); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 4491942951c505..73d8e79069ce31 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -190,7 +190,7 @@ function createCoreRequestHandlerContextMock() { }, elasticsearch: { legacy: { - client: elasticsearchServiceMock.createScopedClusterClient(), + client: elasticsearchServiceMock.createLegacyScopedClusterClient(), }, }, uiSettings: { diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 69fab828e63de4..807d75cd0d701c 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -25,7 +25,7 @@ import { KibanaRequest } from 'kibana/server'; const defaultKibanaIndex = '.kibana'; const savedObjectsClient = savedObjectsClientMock.create(); -const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); +const scopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); const actionExecutor = actionExecutorMock.create(); const executionEnqueuer = jest.fn(); const request = {} as KibanaRequest; diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 1763d275c6fb07..87aa571ce6b8a0 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -37,7 +37,7 @@ const createServicesMock = () => { savedObjectsClient: ReturnType; } > = { - callCluster: elasticsearchServiceMock.createScopedClusterClient().callAsCurrentUser, + callCluster: elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser, getScopedCallCluster: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), }; diff --git a/x-pack/plugins/alerts/server/mocks.ts b/x-pack/plugins/alerts/server/mocks.ts index c94a7aba46cfa3..84f79d53f218cb 100644 --- a/x-pack/plugins/alerts/server/mocks.ts +++ b/x-pack/plugins/alerts/server/mocks.ts @@ -58,7 +58,7 @@ const createAlertServicesMock = () => { alertInstanceFactory: jest .fn, [string]>() .mockReturnValue(alertInstanceFactoryMock), - callCluster: elasticsearchServiceMock.createScopedClusterClient().callAsCurrentUser, + callCluster: elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser, getScopedCallCluster: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), }; diff --git a/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts index 7d86d4fde7e61b..548495866ec210 100644 --- a/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts @@ -20,7 +20,7 @@ export function mockHandlerArguments( { alertsClient = alertsClientMock.create(), listTypes: listTypesRes = [], - esClient = elasticsearchServiceMock.createClusterClient(), + esClient = elasticsearchServiceMock.createLegacyClusterClient(), }: { alertsClient?: AlertsClientMock; listTypes?: AlertType[]; diff --git a/x-pack/plugins/alerts/server/routes/health.test.ts b/x-pack/plugins/alerts/server/routes/health.test.ts index b3f41e03ebdc98..ce782dbd631a5a 100644 --- a/x-pack/plugins/alerts/server/routes/health.test.ts +++ b/x-pack/plugins/alerts/server/routes/health.test.ts @@ -43,7 +43,7 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockReturnValue(Promise.resolve({})); const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']); @@ -72,7 +72,7 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockReturnValue(Promise.resolve({})); const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']); @@ -96,7 +96,7 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockReturnValue(Promise.resolve({})); const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']); @@ -120,7 +120,7 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: {} })); const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']); @@ -144,7 +144,7 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: { enabled: true } })); const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']); @@ -168,7 +168,7 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockReturnValue( Promise.resolve({ security: { enabled: true, ssl: {} } }) ); @@ -194,7 +194,7 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockReturnValue( Promise.resolve({ security: { enabled: true, ssl: { http: { enabled: true } } } }) ); diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts index c1918feb7f4ec3..c2cff83f85f0df 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts @@ -15,7 +15,9 @@ import { const mockRouteContext = ({ core: { - elasticsearch: { legacy: { client: elasticsearchServiceMock.createScopedClusterClient() } }, + elasticsearch: { + legacy: { client: elasticsearchServiceMock.createLegacyScopedClusterClient() }, + }, }, } as unknown) as RequestHandlerContext; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index feec1ee9ba0088..ee6f0a301e9f80 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -18,7 +18,7 @@ let clusterClientAdapter: IClusterClientAdapter; beforeEach(() => { logger = loggingSystemMock.createLogger(); - clusterClient = elasticsearchServiceMock.createClusterClient(); + clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); clusterClientAdapter = new ClusterClientAdapter({ logger, clusterClientPromise: Promise.resolve(clusterClient), diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts index 3fd7e12ed8a0cd..a78e47446fef87 100644 --- a/x-pack/plugins/event_log/server/es/context.test.ts +++ b/x-pack/plugins/event_log/server/es/context.test.ts @@ -17,7 +17,7 @@ let clusterClient: EsClusterClient; beforeEach(() => { logger = loggingSystemMock.createLogger(); - clusterClient = elasticsearchServiceMock.createClusterClient(); + clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); }); describe('createEsContext', () => { diff --git a/x-pack/plugins/global_search/server/services/context.mock.ts b/x-pack/plugins/global_search/server/services/context.mock.ts index 50c6da109f8dde..7c72686529c157 100644 --- a/x-pack/plugins/global_search/server/services/context.mock.ts +++ b/x-pack/plugins/global_search/server/services/context.mock.ts @@ -20,7 +20,7 @@ const createContextMock = () => { }, elasticsearch: { legacy: { - client: elasticsearchServiceMock.createScopedClusterClient(), + client: elasticsearchServiceMock.createLegacyScopedClusterClient(), }, }, uiSettings: { diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts index bf549c18da303f..6e8327e151543e 100644 --- a/x-pack/plugins/licensing/server/plugin.test.ts +++ b/x-pack/plugins/licensing/server/plugin.test.ts @@ -31,11 +31,14 @@ const flushPromises = (ms = 50) => new Promise((res) => setTimeout(res, ms)); function createCoreSetupWith(esClient: ILegacyClusterClient) { const coreSetup = coreMock.createSetup(); - + const coreStart = coreMock.createStart(); coreSetup.getStartServices.mockResolvedValue([ { - ...coreMock.createStart(), - elasticsearch: { legacy: { client: esClient, createClient: jest.fn() } }, + ...coreStart, + elasticsearch: { + ...coreStart.elasticsearch, + legacy: { client: esClient, createClient: jest.fn() }, + }, }, {}, {}, @@ -61,7 +64,7 @@ describe('licensing plugin', () => { }); it('returns license', async () => { - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, @@ -77,7 +80,7 @@ describe('licensing plugin', () => { it('observable receives updated licenses', async () => { const types: LicenseType[] = ['basic', 'gold', 'platinum']; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockImplementation(() => Promise.resolve({ license: buildRawLicense({ type: types.shift() }), @@ -96,7 +99,7 @@ describe('licensing plugin', () => { }); it('returns a license with error when request fails', async () => { - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockRejectedValue(new Error('test')); const coreSetup = createCoreSetupWith(esClient); @@ -109,7 +112,7 @@ describe('licensing plugin', () => { }); it('generate error message when x-pack plugin was not installed', async () => { - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); const error: ElasticsearchError = new Error('reason'); error.status = 400; esClient.callAsInternalUser.mockRejectedValue(error); @@ -127,7 +130,7 @@ describe('licensing plugin', () => { const error1 = new Error('reason-1'); const error2 = new Error('reason-2'); - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser .mockRejectedValueOnce(error1) @@ -145,7 +148,7 @@ describe('licensing plugin', () => { }); it('fetch license immediately without subscriptions', async () => { - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, @@ -161,7 +164,7 @@ describe('licensing plugin', () => { }); it('logs license details without subscriptions', async () => { - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, @@ -187,7 +190,7 @@ describe('licensing plugin', () => { it('generates signature based on fetched license content', async () => { const types: LicenseType[] = ['basic', 'gold', 'basic']; - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockImplementation(() => Promise.resolve({ license: buildRawLicense({ type: types.shift() }), @@ -218,7 +221,7 @@ describe('licensing plugin', () => { api_polling_frequency: moment.duration(50000), }) ); - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, @@ -253,7 +256,7 @@ describe('licensing plugin', () => { }) ); - const esClient = elasticsearchServiceMock.createClusterClient(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); esClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense(), features: {}, @@ -262,7 +265,7 @@ describe('licensing plugin', () => { await plugin.setup(coreSetup); const { createLicensePoller, license$ } = await plugin.start(); - const customClient = elasticsearchServiceMock.createClusterClient(); + const customClient = elasticsearchServiceMock.createLegacyClusterClient(); customClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense({ type: 'gold' }), features: {}, @@ -297,7 +300,7 @@ describe('licensing plugin', () => { await plugin.setup(coreSetup); const { createLicensePoller } = await plugin.start(); - const customClient = elasticsearchServiceMock.createClusterClient(); + const customClient = elasticsearchServiceMock.createLegacyClusterClient(); customClient.callAsInternalUser.mockResolvedValue({ license: buildRawLicense({ type: 'gold' }), features: {}, diff --git a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts index 7ac98196808394..3eee1978d4f1c0 100644 --- a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts +++ b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts @@ -49,7 +49,7 @@ const defaultMockTaskDocs = [getMockTaskInstance()]; export const getMockEs = async ( mockCallWithInternal: LegacyAPICaller = getMockCallWithInternal() ) => { - const client = elasticsearchServiceMock.createClusterClient(); + const client = elasticsearchServiceMock.createLegacyClusterClient(); (client.callAsInternalUser as any) = mockCallWithInternal; return client; }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index d28e95834ca0b3..406d5661c09154 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -28,7 +28,7 @@ describe('ADD remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts, payload }: TestOptions ) => { test(description, async () => { - const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); + const elasticsearchMock = elasticsearchServiceMock.createLegacyClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), @@ -40,10 +40,10 @@ describe('ADD remote clusters', () => { }, }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); elasticsearchServiceMock - .createClusterClient() + .createLegacyClusterClient() .asScoped.mockReturnValue(mockScopedClusterClient); for (const apiResponse of apiResponses) { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index d1e3cf89e94d9b..bd2ad10c4013da 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -30,7 +30,7 @@ describe('DELETE remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts, params }: TestOptions ) => { test(description, async () => { - const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); + const elasticsearchMock = elasticsearchServiceMock.createLegacyClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), @@ -42,10 +42,10 @@ describe('DELETE remote clusters', () => { }, }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); elasticsearchServiceMock - .createClusterClient() + .createLegacyClusterClient() .asScoped.mockReturnValue(mockScopedClusterClient); for (const apiResponse of apiResponses) { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index 24e469c9ec9b2b..910f9e69ea80c8 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -29,7 +29,7 @@ describe('GET remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts }: TestOptions ) => { test(description, async () => { - const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); + const elasticsearchMock = elasticsearchServiceMock.createLegacyClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), @@ -41,10 +41,10 @@ describe('GET remote clusters', () => { }, }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); elasticsearchServiceMock - .createClusterClient() + .createLegacyClusterClient() .asScoped.mockReturnValue(mockScopedClusterClient); for (const apiResponse of apiResponses) { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 9669c98e1349ef..c20ba0a1ec7a9a 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -37,7 +37,7 @@ describe('UPDATE remote clusters', () => { }: TestOptions ) => { test(description, async () => { - const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); + const elasticsearchMock = elasticsearchServiceMock.createLegacyClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), @@ -49,10 +49,10 @@ describe('UPDATE remote clusters', () => { }, }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); elasticsearchServiceMock - .createClusterClient() + .createLegacyClusterClient() .asScoped.mockReturnValue(mockScopedClusterClient); for (const apiResponse of apiResponses) { diff --git a/x-pack/plugins/security/server/authentication/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys.test.ts index 0cdd452d459d1f..631a6f9ab213c3 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.test.ts @@ -24,8 +24,8 @@ describe('API Keys', () => { let mockLicense: jest.Mocked; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createClusterClient(); - mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockClusterClient.asScoped.mockReturnValue( (mockScopedClusterClient as unknown) as jest.Mocked ); diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 3b77ea32481731..300447096af997 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -45,7 +45,7 @@ function getMockOptions({ return { auditLogger: securityAuditLoggerMock.create(), getCurrentUser: jest.fn(), - clusterClient: elasticsearchServiceMock.createClusterClient(), + clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, license: licenseMock.create(), loggers: loggingSystemMock.create(), diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 4157f0598b3d06..56d44e6628a872 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -69,7 +69,7 @@ describe('setupAuthentication()', () => { loggingSystemMock.create().get(), { isTLSEnabled: false } ), - clusterClient: elasticsearchServiceMock.createClusterClient(), + clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), license: licenseMock.create(), loggers: loggingSystemMock.create(), getFeatureUsageService: jest @@ -77,7 +77,7 @@ describe('setupAuthentication()', () => { .mockReturnValue(securityFeatureUsageServiceMock.createStartContract()), }; - mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockSetupAuthenticationParams.clusterClient.asScoped.mockReturnValue( (mockScopedClusterClient as unknown) as jest.Mocked ); diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index 7c71348bb8ca0f..1b574e6e44c100 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -16,7 +16,7 @@ export type MockAuthenticationProviderOptions = ReturnType< export function mockAuthenticationProviderOptions(options?: { name: string }) { return { - client: elasticsearchServiceMock.createClusterClient(), + client: elasticsearchServiceMock.createLegacyClusterClient(), logger: loggingSystemMock.create().get(), basePath: httpServiceMock.createBasePath(), tokens: { refresh: jest.fn(), invalidate: jest.fn() }, diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index 95de8ca9d00e71..22d10d1cec347a 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -43,7 +43,7 @@ describe('BasicAuthenticationProvider', () => { const credentials = { username: 'user', password: 'password' }; const authorization = generateAuthorizationHeader(credentials.username, credentials.password); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -65,7 +65,7 @@ describe('BasicAuthenticationProvider', () => { const authorization = generateAuthorizationHeader(credentials.username, credentials.password); const authenticationError = new Error('Some error'); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -147,7 +147,7 @@ describe('BasicAuthenticationProvider', () => { const user = mockAuthenticatedUser(); const authorization = generateAuthorizationHeader('user', 'password'); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -163,7 +163,7 @@ describe('BasicAuthenticationProvider', () => { const authorization = generateAuthorizationHeader('user', 'password'); const authenticationError = new Error('Forbidden'); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); diff --git a/x-pack/plugins/security/server/authentication/providers/http.test.ts b/x-pack/plugins/security/server/authentication/providers/http.test.ts index e6949269e3fc75..c221ecd3f1e20f 100644 --- a/x-pack/plugins/security/server/authentication/providers/http.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/http.test.ts @@ -126,7 +126,7 @@ describe('HTTPAuthenticationProvider', () => { ]) { const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.asScoped.mockClear(); @@ -156,7 +156,7 @@ describe('HTTPAuthenticationProvider', () => { ]) { const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.asScoped.mockClear(); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index c00374efd59b47..f04506eb01593a 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -47,7 +47,7 @@ describe('KerberosAuthenticationProvider', () => { it('does not handle requests that can be authenticated without `Negotiate` header.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({}); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -61,7 +61,7 @@ describe('KerberosAuthenticationProvider', () => { it('does not handle requests if backend does not support Kerberos.', async () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -82,7 +82,7 @@ describe('KerberosAuthenticationProvider', () => { body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) ); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -101,7 +101,7 @@ describe('KerberosAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ headers: {} }); const failureReason = new errors.ServiceUnavailable(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -118,7 +118,7 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ @@ -153,7 +153,7 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ @@ -257,7 +257,7 @@ describe('KerberosAuthenticationProvider', () => { }); const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ @@ -323,7 +323,7 @@ describe('KerberosAuthenticationProvider', () => { const tokenPair = { accessToken: 'token', refreshToken: 'refresh-token' }; const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.tokens.refresh.mockResolvedValue(null); @@ -355,7 +355,7 @@ describe('KerberosAuthenticationProvider', () => { }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -378,7 +378,7 @@ describe('KerberosAuthenticationProvider', () => { mockOptions.client.asScoped.mockImplementation((scopeableRequest) => { if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -386,7 +386,7 @@ describe('KerberosAuthenticationProvider', () => { } if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); return mockScopedClusterClient; } @@ -423,7 +423,7 @@ describe('KerberosAuthenticationProvider', () => { }; const failureReason = new errors.InternalServerError('Token is not valid!'); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -450,7 +450,7 @@ describe('KerberosAuthenticationProvider', () => { body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) ); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -475,7 +475,7 @@ describe('KerberosAuthenticationProvider', () => { body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) ); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index d787e76628d6db..aea5994e3ba3ef 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -389,7 +389,7 @@ describe('OIDCAuthenticationProvider', () => { }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -446,7 +446,7 @@ describe('OIDCAuthenticationProvider', () => { const authorization = `Bearer ${tokenPair.accessToken}`; const failureReason = new Error('Token is not valid!'); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -466,7 +466,7 @@ describe('OIDCAuthenticationProvider', () => { mockOptions.client.asScoped.mockImplementation((scopeableRequest) => { if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -474,7 +474,7 @@ describe('OIDCAuthenticationProvider', () => { } if (scopeableRequest?.headers.authorization === 'Bearer new-access-token') { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); return mockScopedClusterClient; } @@ -514,7 +514,7 @@ describe('OIDCAuthenticationProvider', () => { const tokenPair = { accessToken: 'expired-token', refreshToken: 'invalid-refresh-token' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -554,7 +554,7 @@ describe('OIDCAuthenticationProvider', () => { '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -602,7 +602,7 @@ describe('OIDCAuthenticationProvider', () => { const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -631,7 +631,7 @@ describe('OIDCAuthenticationProvider', () => { const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index fd014e1a7cb814..fec03c5d04b0d8 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -120,7 +120,7 @@ describe('PKIAuthenticationProvider', () => { }), }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); @@ -162,7 +162,7 @@ describe('PKIAuthenticationProvider', () => { }), }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); @@ -220,7 +220,7 @@ describe('PKIAuthenticationProvider', () => { }); const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); @@ -349,7 +349,7 @@ describe('PKIAuthenticationProvider', () => { }); const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '3A:9A:C5:DD' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); @@ -392,7 +392,7 @@ describe('PKIAuthenticationProvider', () => { }); const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser // In response to call with an expired token. .mockRejectedValueOnce( @@ -436,7 +436,7 @@ describe('PKIAuthenticationProvider', () => { }); const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValueOnce( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -454,7 +454,7 @@ describe('PKIAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() }); const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -480,7 +480,7 @@ describe('PKIAuthenticationProvider', () => { }), }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -509,7 +509,7 @@ describe('PKIAuthenticationProvider', () => { }); const failureReason = new errors.ServiceUnavailable(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index e9af806b36f042..851ecf8107ad20 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -319,7 +319,7 @@ describe('SAMLAuthenticationProvider', () => { beforeEach(() => { mockOptions.basePath.get.mockReturnValue(mockOptions.basePath.serverBasePath); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => Promise.resolve(mockAuthenticatedUser()) ); @@ -448,7 +448,7 @@ describe('SAMLAuthenticationProvider', () => { const authorization = 'Bearer some-valid-token'; const user = mockAuthenticatedUser(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -489,7 +489,7 @@ describe('SAMLAuthenticationProvider', () => { const authorization = `Bearer ${state.accessToken}`; const user = mockAuthenticatedUser(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -543,7 +543,7 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -598,7 +598,7 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -663,7 +663,7 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -1061,7 +1061,7 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -1088,7 +1088,7 @@ describe('SAMLAuthenticationProvider', () => { const authorization = `Bearer ${state.accessToken}`; const failureReason = { statusCode: 500, message: 'Token is not valid!' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -1113,7 +1113,7 @@ describe('SAMLAuthenticationProvider', () => { mockOptions.client.asScoped.mockImplementation((scopeableRequest) => { if (scopeableRequest?.headers.authorization === `Bearer ${state.accessToken}`) { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -1121,7 +1121,7 @@ describe('SAMLAuthenticationProvider', () => { } if (scopeableRequest?.headers.authorization === 'Bearer new-access-token') { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); return mockScopedClusterClient; } @@ -1165,7 +1165,7 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -1199,7 +1199,7 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -1231,7 +1231,7 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -1263,7 +1263,7 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -1304,7 +1304,7 @@ describe('SAMLAuthenticationProvider', () => { redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); 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 ba0f23a3393ae6..f83331d84e43ce 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -49,7 +49,7 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -106,7 +106,7 @@ describe('TokenAuthenticationProvider', () => { }); const authenticationError = new Error('Some error'); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -190,7 +190,7 @@ describe('TokenAuthenticationProvider', () => { const user = mockAuthenticatedUser(); const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -213,7 +213,7 @@ describe('TokenAuthenticationProvider', () => { mockOptions.client.asScoped.mockImplementation((scopeableRequest) => { if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -221,7 +221,7 @@ describe('TokenAuthenticationProvider', () => { } if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); return mockScopedClusterClient; } @@ -256,7 +256,7 @@ describe('TokenAuthenticationProvider', () => { const authorization = `Bearer ${tokenPair.accessToken}`; const authenticationError = new errors.InternalServerError('something went wrong'); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); @@ -274,7 +274,7 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -300,7 +300,7 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -331,7 +331,7 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -362,7 +362,7 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -389,7 +389,7 @@ describe('TokenAuthenticationProvider', () => { const authenticationError = new errors.AuthenticationException('Some error'); mockOptions.client.asScoped.mockImplementation((scopeableRequest) => { if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); @@ -397,7 +397,7 @@ describe('TokenAuthenticationProvider', () => { } if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); return mockScopedClusterClient; } diff --git a/x-pack/plugins/security/server/authentication/tokens.test.ts b/x-pack/plugins/security/server/authentication/tokens.test.ts index 8ad04672fdfade..e8cf37330aff26 100644 --- a/x-pack/plugins/security/server/authentication/tokens.test.ts +++ b/x-pack/plugins/security/server/authentication/tokens.test.ts @@ -18,7 +18,7 @@ describe('Tokens', () => { let tokens: Tokens; let mockClusterClient: jest.Mocked; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); const tokensOptions = { client: mockClusterClient, diff --git a/x-pack/plugins/security/server/authorization/authorization_service.test.ts b/x-pack/plugins/security/server/authorization/authorization_service.test.ts index 4d0ab1c964741d..f67e0863086bb2 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.test.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.test.ts @@ -56,7 +56,7 @@ afterEach(() => { }); it(`#setup returns exposed services`, () => { - const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + const mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); const mockGetSpacesService = jest .fn() .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }); @@ -119,7 +119,7 @@ describe('#start', () => { let licenseSubject: BehaviorSubject; let mockLicense: jest.Mocked; beforeEach(() => { - const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + const mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); licenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures); mockLicense = licenseMock.create(); @@ -221,7 +221,7 @@ describe('#start', () => { }); it('#stop unsubscribes from license and ES updates.', () => { - const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + const mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); const licenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures); const mockLicense = licenseMock.create(); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index 65a3d1bf1650b8..b380f45a12d814 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -21,10 +21,10 @@ const mockActions = { const savedObjectTypes = ['foo-type', 'bar-type']; const createMockClusterClient = (response: any) => { - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(response); - const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + const mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); return { mockClusterClient, mockScopedClusterClient }; @@ -737,7 +737,7 @@ describe('#atSpaces', () => { [`saved_object:${savedObjectTypes[0]}/get`]: false, [`saved_object:${savedObjectTypes[1]}/get`]: true, }, - // @ts-ignore this is wrong on purpose + // @ts-expect-error this is wrong on purpose 'space:space_1': { [mockActions.login]: true, [mockActions.version]: true, @@ -765,7 +765,7 @@ describe('#atSpaces', () => { [mockActions.login]: true, [mockActions.version]: true, }, - // @ts-ignore this is wrong on purpose + // @ts-expect-error this is wrong on purpose 'space:space_1': { [mockActions.login]: true, [mockActions.version]: true, diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts index 0ce7eae932feae..c102af76805b0b 100644 --- a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts @@ -100,7 +100,7 @@ const registerPrivilegesWithClusterTest = ( }; test(description, async () => { - const mockClusterClient = elasticsearchServiceMock.createClusterClient(); + const mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); mockClusterClient.callAsInternalUser.mockImplementation(async (api) => { switch (api) { case 'shield.getPrivilege': { diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 64af6fc857273d..a7b958ee02de50 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -43,7 +43,7 @@ describe('Security Plugin', () => { protocol: 'https', }); - mockClusterClient = elasticsearchServiceMock.createCustomClusterClient(); + mockClusterClient = elasticsearchServiceMock.createLegacyCustomClusterClient(); mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); mockDependencies = ({ diff --git a/x-pack/plugins/security/server/routes/api_keys/get.test.ts b/x-pack/plugins/security/server/routes/api_keys/get.test.ts index f77469552d980a..40065e757e9996 100644 --- a/x-pack/plugins/security/server/routes/api_keys/get.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/get.test.ts @@ -27,7 +27,7 @@ describe('Get API keys', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts index 2889cf78aff83f..33c52688ce8e3d 100644 --- a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts @@ -27,7 +27,7 @@ describe('Invalidate API keys', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); for (const apiResponse of apiResponses) { mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts index afb67dc3bbfca1..a506cc6306c535 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -48,7 +48,7 @@ describe('Check API keys privileges', () => { apiKeys.areAPIKeysEnabled() ); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); for (const apiResponse of callAsCurrentUserResponses) { mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts index ada6a1c8d2dc3d..399f79f44744d2 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts @@ -30,7 +30,7 @@ describe('DELETE role', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts index 49123fe9c74d71..d9062bcfa2efe1 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -33,7 +33,7 @@ describe('GET role', () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts index 5dbe8682c54269..66e8086d49c666 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -33,7 +33,7 @@ describe('GET all roles', () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); mockRouteDefinitionParams.authz.applicationName = application; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); if (apiResponse) { mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index bec60fa149bcf9..8f115f11329d3f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -72,7 +72,7 @@ const putRoleTest = ( mockRouteDefinitionParams.authz.applicationName = application; mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); for (const apiResponse of apiResponses) { mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); diff --git a/x-pack/plugins/security/server/routes/index.mock.ts b/x-pack/plugins/security/server/routes/index.mock.ts index c7ff2a1e68b027..24de2af5e9703c 100644 --- a/x-pack/plugins/security/server/routes/index.mock.ts +++ b/x-pack/plugins/security/server/routes/index.mock.ts @@ -21,7 +21,7 @@ export const routeDefinitionParamsMock = { basePath: httpServiceMock.createBasePath(), csp: httpServiceMock.createSetupContract().csp, logger: loggingSystemMock.create().get(), - clusterClient: elasticsearchServiceMock.createClusterClient(), + clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), { isTLSEnabled: false, }), diff --git a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts index 34961dbe27675e..aec0310129f6ee 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/delete.test.ts @@ -13,7 +13,7 @@ describe('DELETE role mappings', () => { it('allows a role mapping to be deleted', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ acknowledged: true }); diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts index 8070b3371fcb38..ee1d550bbe24d5 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts @@ -76,7 +76,7 @@ describe('GET role mappings feature check', () => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation( internalUserClusterClientImpl diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts index e0df59ebe7a00d..9af7268a57f9ce 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.test.ts @@ -53,7 +53,7 @@ describe('GET role mappings', () => { it('returns all role mappings', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(mockRoleMappingResponse); @@ -128,7 +128,7 @@ describe('GET role mappings', () => { it('returns role mapping by name', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ mapping1: { @@ -216,7 +216,7 @@ describe('GET role mappings', () => { it('returns a 404 when the role mapping is not found', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( Boom.notFound('role mapping not found!') diff --git a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts index ed3d1bbd0fca2b..8f61d2a122f0c1 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/post.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/post.test.ts @@ -13,7 +13,7 @@ describe('POST role mappings', () => { it('allows a role mapping to be created', async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({ created: true }); diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index 721c020c7431b5..21c7fc13404371 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -56,7 +56,7 @@ describe('Change password', () => { provider: { type: 'basic', name: 'basic' }, }); - mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockClusterClient = routeParamsMock.clusterClient; mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts index 540976134d8ae1..863a1d50377565 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts @@ -85,8 +85,8 @@ describe('test alerts route', () => { let ingestSavedObjectClient: jest.Mocked; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createClusterClient(); - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 668911b8d1f296..42cce382ec20ca 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -59,10 +59,10 @@ describe('test endpoint route', () => { }; beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< + mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked< ILegacyClusterClient >; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 0578f795f4a4e5..8d4524e06c49ff 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -32,7 +32,7 @@ describe('test policy response handler', () => { let mockResponse: jest.Mocked; beforeEach(() => { - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 7289eb6dea1610..c45dd5bd8a2817 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -16,7 +16,7 @@ import { siemMock } from '../../../../mocks'; const createMockClients = () => ({ alertsClient: alertsClientMock.create(), - clusterClient: elasticsearchServiceMock.createScopedClusterClient(), + clusterClient: elasticsearchServiceMock.createLegacyScopedClusterClient(), licensing: { license: licensingMock.createLicenseMock() }, savedObjectsClient: savedObjectsClientMock.create(), appClient: siemMock.createClient(), diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts index f044022d6db69e..e9b692e4731aab 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/mocks.ts @@ -7,7 +7,7 @@ import { MlPluginSetup } from '../../../../ml/server'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; -const createMockClient = () => elasticsearchServiceMock.createClusterClient(); +const createMockClient = () => elasticsearchServiceMock.createLegacyClusterClient(); const createMockMlSystemProvider = () => jest.fn(() => ({ mlCapabilities: jest.fn(), diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts index 4ce21f1b311e81..b0dec299b2b12c 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_deprecation_logging_apis.test.ts @@ -12,7 +12,7 @@ import { describe('getDeprecationLoggingStatus', () => { it('calls cluster.getSettings', async () => { - const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); await getDeprecationLoggingStatus(dataClient); expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.getSettings', { includeDefaults: true, @@ -23,7 +23,7 @@ describe('getDeprecationLoggingStatus', () => { describe('setDeprecationLogging', () => { describe('isEnabled = true', () => { it('calls cluster.putSettings with logger.deprecation = WARN', async () => { - const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); await setDeprecationLogging(dataClient, true); expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.putSettings', { body: { transient: { 'logger.deprecation': 'WARN' } }, @@ -33,7 +33,7 @@ describe('setDeprecationLogging', () => { describe('isEnabled = false', () => { it('calls cluster.putSettings with logger.deprecation = ERROR', async () => { - const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); await setDeprecationLogging(dataClient, false); expect(dataClient.callAsCurrentUser).toHaveBeenCalledWith('cluster.putSettings', { body: { transient: { 'logger.deprecation': 'ERROR' } }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index 89571a4a18231a..2a4fa5cd48ded2 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -14,7 +14,7 @@ import fakeDeprecations from './__fixtures__/fake_deprecations.json'; describe('getUpgradeAssistantStatus', () => { let deprecationsResponse: DeprecationAPIResponse; - const dataClient = elasticsearchServiceMock.createScopedClusterClient(); + const dataClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); (dataClient.callAsCurrentUser as jest.Mock).mockImplementation(async (api, { path, index }) => { if (path === '/_migration/deprecations') { return deprecationsResponse; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 7188241e10f9a2..e14056439ca6b2 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -21,7 +21,7 @@ describe('Upgrade Assistant Usage Collector', () => { let clusterClient: ILegacyClusterClient; beforeEach(() => { - clusterClient = elasticsearchServiceMock.createClusterClient(); + clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); (clusterClient.callAsInternalUser as jest.Mock).mockResolvedValue({ persistent: {}, transient: { diff --git a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts index 861ef2d3968dc4..2df770c3ce45c0 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/__mocks__/routes.mock.ts @@ -13,7 +13,7 @@ export const routeHandlerContextMock = ({ core: { elasticsearch: { legacy: { - client: elasticsearchServiceMock.createScopedClusterClient(), + client: elasticsearchServiceMock.createLegacyScopedClusterClient(), }, }, savedObjects: { client: savedObjectsClientMock.create() }, diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index 17bbb051b1ab11..2a1417b49dca41 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -51,7 +51,7 @@ type MockCallES = (method: any, params: any) => Promise; const setupMock = ( criteria: MultiPageCriteria[] ): [MockCallES, jest.Mocked>] => { - const esMock = elasticsearchServiceMock.createScopedClusterClient(); + const esMock = elasticsearchServiceMock.createLegacyScopedClusterClient(); criteria.forEach(({ after_key, bucketCriteria }) => { const mockResponse = { From e58cc173f1cf03946365914de288e1ea961ecf6c Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 7 Jul 2020 05:41:50 -0700 Subject: [PATCH 35/57] Fix Data Streams and Rollups Jobs deep-link bugs (#70903) * Add extractQueryParams to es_ui_shared/public/url. Update CCR, Remote Clusters, and Rollup to consume this service via shared_imports. * Fix Data Streams bug in which clicking a data stream would apply a deep-link filter to the table. * Fix Rollup Job deep-link bug. --- src/plugins/es_ui_shared/public/index.ts | 2 ++ .../public/url/extract_query_params.ts | 29 +++++++++++++++++++ src/plugins/es_ui_shared/public/url/index.ts | 20 +++++++++++++ .../components/auto_follow_pattern_form.js | 3 +- .../follower_index_form.js | 7 ++--- .../auto_follow_pattern_list.js | 2 +- .../follower_indices_list.js | 2 +- .../auto_follow_pattern_validators.js | 2 +- .../public/app/services/input_validation.js | 3 +- .../query_params.js => shared_imports.ts} | 11 +------ .../data_stream_list/data_stream_list.tsx | 9 ++++-- .../index_list/index_table/index_table.js | 1 + .../index_management/public/shared_imports.ts | 1 + .../remote_cluster_add/remote_cluster_add.js | 3 +- .../remote_cluster_edit.js | 3 +- .../remote_cluster_list.js | 2 +- .../public/application/services/index.js | 2 -- .../application/store/actions/add_cluster.js | 8 ++--- .../application/store/actions/detail_panel.js | 3 +- .../application/store/actions/edit_cluster.js | 8 ++--- .../query_params.js => shared_imports.ts} | 11 +------ .../crud_app/sections/job_list/job_list.js | 5 ++-- .../sections/job_list/job_table/job_table.js | 2 +- .../rollup/public/crud_app/services/index.js | 2 -- .../public/crud_app/services/query_params.js | 23 --------------- .../crud_app/store/actions/create_job.js | 2 +- .../crud_app/store/actions/detail_panel.js | 3 +- .../plugins/rollup/public/shared_imports.ts | 2 +- 28 files changed, 89 insertions(+), 82 deletions(-) create mode 100644 src/plugins/es_ui_shared/public/url/extract_query_params.ts create mode 100644 src/plugins/es_ui_shared/public/url/index.ts rename x-pack/plugins/cross_cluster_replication/public/{app/services/query_params.js => shared_imports.ts} (51%) rename x-pack/plugins/remote_clusters/public/{application/services/query_params.js => shared_imports.ts} (51%) delete mode 100644 x-pack/plugins/rollup/public/crud_app/services/query_params.js diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 67c1ee3c7d6779..d472b7e462057f 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -68,6 +68,8 @@ export { export { Monaco, Forms }; +export { extractQueryParams } from './url'; + /** dummy plugin, we just want esUiShared to have its own bundle */ export function plugin() { return new (class EsUiSharedPlugin { diff --git a/src/plugins/es_ui_shared/public/url/extract_query_params.ts b/src/plugins/es_ui_shared/public/url/extract_query_params.ts new file mode 100644 index 00000000000000..09789e0f32bf20 --- /dev/null +++ b/src/plugins/es_ui_shared/public/url/extract_query_params.ts @@ -0,0 +1,29 @@ +/* + * 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 { parse, ParsedQuery } from 'query-string'; + +export function extractQueryParams(queryString: string = ''): ParsedQuery { + const hrefSplit = queryString.split('?'); + if (!hrefSplit.length) { + return {}; + } + + return parse(hrefSplit[1], { sort: false }); +} diff --git a/src/plugins/es_ui_shared/public/url/index.ts b/src/plugins/es_ui_shared/public/url/index.ts new file mode 100644 index 00000000000000..692e094f9eda49 --- /dev/null +++ b/src/plugins/es_ui_shared/public/url/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 { extractQueryParams } from './extract_query_params'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js index 7874f6ac649eb9..74894b0cb87444 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js @@ -29,11 +29,10 @@ import { EuiTitle, } from '@elastic/eui'; -import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; import { indexPatterns } from '../../../../../../src/plugins/data/public'; +import { extractQueryParams, indices } from '../../shared_imports'; import { routing } from '../services/routing'; -import { extractQueryParams } from '../services/query_params'; import { getRemoteClusterName } from '../services/get_remote_cluster_name'; import { API_STATUS } from '../constants'; import { SectionError } from './section_error'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js index 28673c55fd031c..a545aec63e222b 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js @@ -28,12 +28,14 @@ import { EuiTitle, } from '@elastic/eui'; -import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; +import { extractQueryParams, indices } from '../../../shared_imports'; import { indexNameValidator, leaderIndexValidator } from '../../services/input_validation'; import { routing } from '../../services/routing'; import { getFatalErrors } from '../../services/notifications'; import { loadIndices } from '../../services/api'; import { API_STATUS } from '../../constants'; +import { getRemoteClusterName } from '../../services/get_remote_cluster_name'; +import { RemoteClustersFormField } from '../remote_clusters_form_field'; import { SectionError } from '../section_error'; import { FormEntryRow } from '../form_entry_row'; import { @@ -41,9 +43,6 @@ import { emptyAdvancedSettings, areAdvancedSettingsEdited, } from './advanced_settings_fields'; -import { extractQueryParams } from '../../services/query_params'; -import { getRemoteClusterName } from '../../services/get_remote_cluster_name'; -import { RemoteClustersFormField } from '../remote_clusters_form_field'; import { FollowerIndexRequestFlyout } from './follower_index_request_flyout'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js index 5ef78b9ba6bb5b..33d01bbe38a7ff 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; -import { extractQueryParams } from '../../../services/query_params'; +import { extractQueryParams } from '../../../../shared_imports'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants'; import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js index 4d4cbbf6825ec0..2ceb410e61cccf 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; -import { extractQueryParams } from '../../../services/query_params'; +import { extractQueryParams } from '../../../../shared_imports'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants'; import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components'; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js index 39d40389daa175..621d299b7f151f 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js @@ -8,8 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; import { indexPatterns } from '../../../../../../src/plugins/data/public'; +import { indices } from '../../shared_imports'; const { indexNameBeginsWithPeriod, diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js index e702a47e911558..0feccbeafefbd4 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js @@ -6,7 +6,8 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; + +import { indices } from '../../shared_imports'; const isEmpty = (value) => { return !value || !value.trim().length; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js b/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts similarity index 51% rename from x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js rename to x-pack/plugins/cross_cluster_replication/public/shared_imports.ts index af462bfeffcf50..2ff4bd988798a1 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js +++ b/x-pack/plugins/cross_cluster_replication/public/shared_imports.ts @@ -4,13 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; - -export function extractQueryParams(queryString) { - const hrefSplit = queryString.split('?'); - if (!hrefSplit.length) { - return {}; - } - - return parse(hrefSplit[1], { sort: false }); -} +export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index bad008b665cfbf..adfaa7820aff30 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; -import { reactRouterNavigate } from '../../../../shared_imports'; +import { reactRouterNavigate, extractQueryParams } from '../../../../shared_imports'; import { useAppContext } from '../../../app_context'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadDataStreams } from '../../../services/api'; @@ -28,8 +28,11 @@ export const DataStreamList: React.FunctionComponent { + const { isDeepLink } = extractQueryParams(search); + const { core: { getUrlForApp }, plugins: { ingestManager }, @@ -144,7 +147,9 @@ export const DataStreamList: React.FunctionComponent {value} diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index ad221ae73fecf5..5bf1a31d0902bf 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -11,6 +11,7 @@ export { sendRequest, useRequest, Forms, + extractQueryParams, } from '../../../../src/plugins/es_ui_shared/public/'; export { diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js index b13e833f60b188..cc0e5ba93011a9 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js @@ -10,7 +10,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageContent } from '@elastic/eui'; -import { getRouter, redirect, extractQueryParams } from '../../services'; +import { extractQueryParams } from '../../../shared_imports'; +import { getRouter, redirect } from '../../services'; import { setBreadcrumbs } from '../../services/breadcrumb'; import { RemoteClusterPageTitle, RemoteClusterForm } from '../components'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js index 9018647600b8df..34622055b1eaa5 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js @@ -21,7 +21,8 @@ import { } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; -import { extractQueryParams, getRouter, redirect } from '../../services'; +import { extractQueryParams } from '../../../shared_imports'; +import { getRouter, redirect } from '../../services'; import { setBreadcrumbs } from '../../services/breadcrumb'; import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js index 6d40cbbeb82aec..c8fdd94b881bce 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js @@ -29,7 +29,7 @@ import { } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; -import { extractQueryParams } from '../../services'; +import { extractQueryParams } from '../../../shared_imports'; import { setBreadcrumbs } from '../../services/breadcrumb'; import { RemoteClusterTable } from './remote_cluster_table'; diff --git a/x-pack/plugins/remote_clusters/public/application/services/index.js b/x-pack/plugins/remote_clusters/public/application/services/index.js index ce8d06b6e22788..68edec7904205f 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/index.js +++ b/x-pack/plugins/remote_clusters/public/application/services/index.js @@ -12,8 +12,6 @@ export { initRedirect, redirect } from './redirect'; export { isAddressValid, isPortValid } from './validate_address'; -export { extractQueryParams } from './query_params'; - export { setUserHasLeftApp, getUserHasLeftApp, registerRouter, getRouter } from './routing'; export { trackUiMetric, METRIC_TYPE } from './ui_metric'; diff --git a/x-pack/plugins/remote_clusters/public/application/store/actions/add_cluster.js b/x-pack/plugins/remote_clusters/public/application/store/actions/add_cluster.js index d57fd37e791a1f..9650aaacf4bec1 100644 --- a/x-pack/plugins/remote_clusters/public/application/store/actions/add_cluster.js +++ b/x-pack/plugins/remote_clusters/public/application/store/actions/add_cluster.js @@ -6,12 +6,8 @@ import { i18n } from '@kbn/i18n'; -import { - addCluster as sendAddClusterRequest, - getRouter, - extractQueryParams, - redirect, -} from '../../services'; +import { extractQueryParams } from '../../../shared_imports'; +import { addCluster as sendAddClusterRequest, getRouter, redirect } from '../../services'; import { fatalError, toasts } from '../../services/notification'; import { diff --git a/x-pack/plugins/remote_clusters/public/application/store/actions/detail_panel.js b/x-pack/plugins/remote_clusters/public/application/store/actions/detail_panel.js index 57e8876faca2be..a5b023166da7c7 100644 --- a/x-pack/plugins/remote_clusters/public/application/store/actions/detail_panel.js +++ b/x-pack/plugins/remote_clusters/public/application/store/actions/detail_panel.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { extractQueryParams, getRouter } from '../../services'; +import { extractQueryParams } from '../../../shared_imports'; +import { getRouter } from '../../services'; import { OPEN_DETAIL_PANEL, CLOSE_DETAIL_PANEL } from '../action_types'; export const openDetailPanel = ({ name }) => (dispatch) => { diff --git a/x-pack/plugins/remote_clusters/public/application/store/actions/edit_cluster.js b/x-pack/plugins/remote_clusters/public/application/store/actions/edit_cluster.js index 4fd8faeb7021e1..0e18dd8a5136c7 100644 --- a/x-pack/plugins/remote_clusters/public/application/store/actions/edit_cluster.js +++ b/x-pack/plugins/remote_clusters/public/application/store/actions/edit_cluster.js @@ -9,12 +9,8 @@ import { i18n } from '@kbn/i18n'; import { toasts, fatalError } from '../../services/notification'; import { loadClusters } from './load_clusters'; -import { - editCluster as sendEditClusterRequest, - extractQueryParams, - getRouter, - redirect, -} from '../../services'; +import { extractQueryParams } from '../../../shared_imports'; +import { editCluster as sendEditClusterRequest, getRouter, redirect } from '../../services'; import { EDIT_CLUSTER_START, diff --git a/x-pack/plugins/remote_clusters/public/application/services/query_params.js b/x-pack/plugins/remote_clusters/public/shared_imports.ts similarity index 51% rename from x-pack/plugins/remote_clusters/public/application/services/query_params.js rename to x-pack/plugins/remote_clusters/public/shared_imports.ts index af462bfeffcf50..2ff4bd988798a1 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/query_params.js +++ b/x-pack/plugins/remote_clusters/public/shared_imports.ts @@ -4,13 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'query-string'; - -export function extractQueryParams(queryString) { - const hrefSplit = queryString.split('?'); - if (!hrefSplit.length) { - return {}; - } - - return parse(hrefSplit[1], { sort: false }); -} +export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js index 85cd6e742d27fa..4c1f928197ad00 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.js @@ -27,10 +27,9 @@ import { import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { getRouterLinkProps, extractQueryParams, listBreadcrumb } from '../../services'; - +import { extractQueryParams } from '../../../shared_imports'; +import { getRouterLinkProps, listBreadcrumb } from '../../services'; import { JobTable } from './job_table'; - import { DetailPanel } from './detail_panel'; const REFRESH_RATE_MS = 30000; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js index 6337e6812ca4ba..66ecb37d684399 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js @@ -265,7 +265,7 @@ export class JobTable extends Component { { trackUiMetric(METRIC_TYPE.CLICK, UIM_SHOW_DETAILS_CLICK); - openDetailPanel(job.id); + openDetailPanel(encodeURIComponent(job.id)); }} > {value} diff --git a/x-pack/plugins/rollup/public/crud_app/services/index.js b/x-pack/plugins/rollup/public/crud_app/services/index.js index 0b45b1bdb6b5fb..6593c0dbcbfa49 100644 --- a/x-pack/plugins/rollup/public/crud_app/services/index.js +++ b/x-pack/plugins/rollup/public/crud_app/services/index.js @@ -33,8 +33,6 @@ export { serializeJob, deserializeJob, deserializeJobs } from './jobs'; export { createNoticeableDelay } from './noticeable_delay'; -export { extractQueryParams } from './query_params'; - export { setUserHasLeftApp, getUserHasLeftApp, diff --git a/x-pack/plugins/rollup/public/crud_app/services/query_params.js b/x-pack/plugins/rollup/public/crud_app/services/query_params.js deleted file mode 100644 index bdb5f5bed5c63e..00000000000000 --- a/x-pack/plugins/rollup/public/crud_app/services/query_params.js +++ /dev/null @@ -1,23 +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. - */ - -export function extractQueryParams(queryString) { - if (!queryString || queryString.trim().length === 0) { - return {}; - } - - const extractedQueryParams = {}; - const queryParamPairs = queryString - .split('?')[1] - .split('&') - .map((paramString) => paramString.split('=')); - - queryParamPairs.forEach(([key, value]) => { - extractedQueryParams[key] = decodeURIComponent(value); - }); - - return extractedQueryParams; -} diff --git a/x-pack/plugins/rollup/public/crud_app/store/actions/create_job.js b/x-pack/plugins/rollup/public/crud_app/store/actions/create_job.js index c404471f803f35..6b6a0e732eb859 100644 --- a/x-pack/plugins/rollup/public/crud_app/store/actions/create_job.js +++ b/x-pack/plugins/rollup/public/crud_app/store/actions/create_job.js @@ -102,7 +102,7 @@ export const createJob = (jobConfig) => async (dispatch) => { // here, because it would partially obscure the detail panel. getRouter().history.push({ pathname: `/job_list`, - search: `?job=${jobConfig.id}`, + search: `?job=${encodeURIComponent(jobConfig.id)}`, }); }; diff --git a/x-pack/plugins/rollup/public/crud_app/store/actions/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/store/actions/detail_panel.js index d01bc6b49c94c5..1178cc3e79df8b 100644 --- a/x-pack/plugins/rollup/public/crud_app/store/actions/detail_panel.js +++ b/x-pack/plugins/rollup/public/crud_app/store/actions/detail_panel.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { extractQueryParams, getRouter } from '../../services'; +import { extractQueryParams } from '../../../shared_imports'; +import { getRouter } from '../../services'; import { OPEN_DETAIL_PANEL, CLOSE_DETAIL_PANEL } from '../action_types'; export const openDetailPanel = ({ panelType, jobId }) => (dispatch) => { diff --git a/x-pack/plugins/rollup/public/shared_imports.ts b/x-pack/plugins/rollup/public/shared_imports.ts index 1ac25a1a0e5f8e..2ff4bd988798a1 100644 --- a/x-pack/plugins/rollup/public/shared_imports.ts +++ b/x-pack/plugins/rollup/public/shared_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { indices } from '../../../../src/plugins/es_ui_shared/public'; +export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public'; From 32190758fb4d64ebc8d7995a5dd3e29daa88d40c Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 7 Jul 2020 14:03:25 +0100 Subject: [PATCH 36/57] skip flaky suite (#70757) --- .../cypress/integration/events_viewer.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index 84ca1e20e95766..843d99cf06cab5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -46,7 +46,8 @@ const defaultHeadersInDefaultEcsCategory = [ { id: 'destination.ip' }, ]; -describe('Events Viewer', () => { +// Flakky: https://github.com/elastic/kibana/issues/70757 +describe.skip('Events Viewer', () => { context('Fields rendering', () => { before(() => { loginAndWaitForPage(HOSTS_URL); From f30417624b3ef00a5a60ecbf404311cf3639146a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 7 Jul 2020 16:14:13 +0300 Subject: [PATCH 37/57] fix flaky test on tsvb switch index patterns (#70811) Co-authored-by: Elastic Machine --- test/functional/apps/visualize/_tsvb_chart.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 7e22f543bc7dbf..191572e3e13541 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -31,7 +31,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { this.tags('includeFirefox'); beforeEach(async () => { - await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); + await security.testUser.setRoles([ + 'kibana_admin', + 'test_logstash_reader', + 'kibana_sample_admin', + ]); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); @@ -105,15 +109,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/43150 - describe.skip('switch index patterns', () => { + describe('switch index patterns', () => { beforeEach(async () => { log.debug('Load kibana_sample_data_flights data'); await esArchiver.loadIfNeeded('kibana_sample_data_flights'); await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); await PageObjects.visualBuilder.checkMetricTabIsPresent(); - await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); }); after(async () => { await security.testUser.restoreDefaults(); From 7026a50f52dcf059896c62efd73fe96ba6cfbad0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2020 15:42:53 +0200 Subject: [PATCH 38/57] Update dependency @elastic/charts to v19.8.0 (#70803) Co-authored-by: Renovate Bot --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2f6b643b026015..bb28c9e27e9f7b 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.10.1", "@babel/register": "^7.10.1", "@elastic/apm-rum": "^5.2.0", - "@elastic/charts": "19.7.0", + "@elastic/charts": "19.8.0", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", "@elastic/eui": "24.1.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 5f306cd5128b9f..f4d9beb038966e 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "19.7.0", + "@elastic/charts": "19.8.0", "@elastic/eui": "24.1.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/yarn.lock b/yarn.lock index 5efea82e84c68b..ac5f653fdf3d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2144,10 +2144,10 @@ dependencies: "@elastic/apm-rum-core" "^5.3.0" -"@elastic/charts@19.7.0": - version "19.7.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.7.0.tgz#86cdee072d70e641135de99646c90359992bfdf0" - integrity sha512-oNAPOpI9OkuX/pWL+SGShcmdAUB1mwbOyJnp9/PHFqXtARg3aaiTDD0olZUuynGKd6DWnN8mEAiwoe7nsWGP9g== +"@elastic/charts@19.8.0": + version "19.8.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.8.0.tgz#d8439288e2574053ca9e6eee6f3b00bf04917803" + integrity sha512-px0mX0UBtFhbt5O4JAqOZPYC+K9avVmjgKPoIqQBMnnwkKtuKGH1mQ7XZro3E7COJ4WQ5nGxWtC+ewlFQP3zww== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 8ee4945a4365d96bec99dc9144e7302085f4d542 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 7 Jul 2020 06:52:06 -0700 Subject: [PATCH 39/57] =?UTF-8?q?fix:=20=F0=9F=90=9B=20remove=20inspector?= =?UTF-8?q?=20plugin=20dependency=20on=20share=20plugin=20(#70783)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inspector/public/views/data/components/data_table.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugins/inspector/public/views/data/components/data_table.tsx b/src/plugins/inspector/public/views/data/components/data_table.tsx index 0fdf3d9b13e333..69be069272f79c 100644 --- a/src/plugins/inspector/public/views/data/components/data_table.tsx +++ b/src/plugins/inspector/public/views/data/components/data_table.tsx @@ -37,7 +37,6 @@ import { DataDownloadOptions } from './download_options'; import { DataViewRow, DataViewColumn } from '../types'; import { TabularData } from '../../../../common/adapters/data/types'; import { IUiSettingsClient } from '../../../../../../core/public'; -import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../../share/public'; interface DataTableFormatState { columns: DataViewColumn[]; @@ -59,8 +58,8 @@ export class DataTableFormat extends Component Date: Tue, 7 Jul 2020 06:52:17 -0700 Subject: [PATCH 40/57] =?UTF-8?q?fix:=20=F0=9F=90=9B=20revert=20back=20opt?= =?UTF-8?q?imistic=20changes=20if=20IP=20update=20failed=20(#70794)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 revert back optimistic changes if IP update failed * fix: 🐛 use correct type for index pattern field --- .../components/field_editor/field_editor.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 5ae50098e79e7c..99ef83604239a5 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -802,7 +802,10 @@ export class FieldEditor extends PureComponent f.name === field.name); + let oldField: IFieldType | undefined; + if (index > -1) { + oldField = indexPattern.fields.getByName(field.name); indexPattern.fields.update(field); } else { indexPattern.fields.add(field); @@ -814,14 +817,23 @@ export class FieldEditor extends PureComponent { - const message = i18n.translate('indexPatternManagement.deleteField.savedHeader', { - defaultMessage: "Saved '{fieldName}'", - values: { fieldName: field.name }, + return indexPattern + .save() + .then(() => { + const message = i18n.translate('indexPatternManagement.deleteField.savedHeader', { + defaultMessage: "Saved '{fieldName}'", + values: { fieldName: field.name }, + }); + this.context.services.notifications.toasts.addSuccess(message); + redirectAway(); + }) + .catch((error) => { + if (oldField) { + indexPattern.fields.update(oldField); + } else { + indexPattern.fields.remove(field); + } }); - this.context.services.notifications.toasts.addSuccess(message); - redirectAway(); - }); }; isSavingDisabled() { From 0bae5d62c932c670b9da55575fbf5caaffbc88e5 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 7 Jul 2020 16:01:06 +0200 Subject: [PATCH 41/57] [Discover] Doc Table functional tests (#70776) Co-authored-by: Elastic Machine --- .../public/application/angular/discover.html | 1 + .../components/table_row/details.html | 1 + .../skip_bottom_button/skip_bottom_button.tsx | 1 + test/functional/apps/discover/_doc_table.ts | 161 ++++++++++++++++++ test/functional/apps/discover/index.js | 1 + test/functional/page_objects/discover_page.ts | 39 ++++- 6 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 test/functional/apps/discover/_doc_table.ts diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html index 3c16e4a6d9deee..48a8442b063160 100644 --- a/src/plugins/discover/public/application/angular/discover.html +++ b/src/plugins/discover/public/application/angular/discover.html @@ -135,6 +135,7 @@

{{screenTitle}}