From da4c42da48b8bb62a743f898115f3aab963bfadb Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 21 Apr 2023 11:04:09 -0700 Subject: [PATCH 01/65] [DOCS] Automate email connector screenshot, edit secrets field label (#155464) --- .../connectors/action-types/email.asciidoc | 1 + .../connectors/images/email-connector.png | Bin 188529 -> 178393 bytes .../encrypted_fields_callout.test.tsx | 6 +++--- .../encrypted_fields_callout.tsx | 2 +- .../stack_connectors/connector_types.ts | 12 ++++++++++++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 65bc85a7d7133a..f04888a681b6ae 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -30,6 +30,7 @@ or as needed when you're creating a rule. For example: [role="screenshot"] image::management/connectors/images/email-connector.png[Email connector] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. [float] [[email-connector-configuration]] diff --git a/docs/management/connectors/images/email-connector.png b/docs/management/connectors/images/email-connector.png index b837fa545a4d111c8d0f02b05cd7cba70d91616c..3cce19021937fe2e838b666138411912be9db0e2 100644 GIT binary patch literal 178393 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

GeT6LXq5lp8i7?7e?)%Xb=lkmTTS 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 G_y0dzQW4Yu literal 188529 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

+`T!qfRkMFLyTo=(c*rSq%mg;( z8U=bPc?z;}a6}B~LmRcl78-D+d8K@cJzN!&_KWX4E7Phd_khqkcVRPcHw|_1W(=Sa zu!*?MMX`cs<&hl+E9?b2unlXfx}1UC9u@Hf)NTa%FVO8ae5GPUW4PRgg1mQcA%nGK zxpUU>%WLl>Tyy&c-y;#7fhaaPdWTA0+s@v86*ydY;W%Q#Tg7L0QcehH#C^J>sjlOB zAiUD4odUfv;j>xLZm<^UF!H6f{pS@UME=2I@{6%jL+&0*<>J-2tcP??TdJy^Euem9 zJY1$?T)2H_QB>%whnT>a^HcQmjJfR^WX1j=P#>89U2}~TdjkmmdzJrd`OOvox(T{- zDdPvsuX^oHUR~@v;4>?mVDiSusJcdv@q*geBVlV;1g|p?%Wp|N8JoF-R5%J(5#u3n#35Cf2ndFlh;!Tt68lb z!;^6y61Dg>HKnjGcg}lxJ3emX+jQtNu8vZ~W1!+-vcILW(nsTUywiqOHk_N-U;I*D z7L(kQyh`sCnCNP0=XrOjv|?vRlJBAYUKH-kJ1)}^DbOXM>BVVYnwcgNGL z>?fL`pKHa%(#S-kl9#_j2>mrD{{pJM+G|bneCvi@ve3BasJ68+$OOlyZ&98G#4o(=&w9@%Rx2l1PS{U#tRe(r2MnbhRO`*7 zeMK#(3OF^I9=Svu4;%P1S3{LR9&|EWn{acU%bt@9usT*R71i11Q3itcNJ^( zbT!jQlU38&X}F_?Iz&)vCTmi-7V6u^suLDZ#qD2ed#r7Cpl%f@dvNqK-7`9Y%q==L zulOgL{03tChc)^IT>U#s&>$}|mP<-&PmTlMXV{K>Y;)MyHIg!(Ywt9ckq?S6 zNk{VCnyQ?BAFc-dlWYe8yYxoe3TPA$mgA{(6Mt+oDoEi<>7sCH(7pj5GYLCMV|y z-+Dh$aBr{LqlfNxO_Or;NaL#uMPm#eYhoy78a@4L=eXrj2U0T;$k26d7j@gewE#RH z^Ir8x@d&899g%;0o_b-eUK)x-$g3W)mC=5vABAPttX8U*5mcZ}Dn)JWvQt(mMqFiI zy|00nt`3atkFQz)f^JD$p{KAEuUOno4xNff9sDuo#z1k4CjPd`F~~T#AF-#D4Yi&Y zpK_UNoU$C9du89%+7c=^7jO~UD%D%D`m_oTMRmHT@DpHmrXH%n`z#WfBg#n|$urLo zWp!x96n^iyyDW{|RoAb&^o`NR28KLU}kqmvVYbh5mYiz)LyWa_I}3{ZDdj}GY;yF$it zA35}5`dx|_kCgKL+>*plTbmenM^qJ1sUPxb)=NsetRZE~K|bppblf-E$z<%J3sIqi zjI>#l6^Ig*#rUC3b{AAt-bUYYREg1ZC=aI5-1A;uzJkCHDjRF68PE8>FL9v@E(@Fo)AO++6&Cx;CY}92UCS%m96;y6%?eu;08fJ6hEt zS~4(_q7FJABPtCNcRy?`dgcx(@fy@8AH4Ryu(`Mc1EicS#vtK0;FzE#|e$f@T`-u!OXS3jy(=TRPM$@pVGWdg-(l3wOs zvLpRDdng6UmS9(3Rc9{7Psc1+QqJr8SAb`8sbP06Z%9?C)FsdjAgE&|t0!hPiR?X| z4`K&@e-{%wy}0j|ZI(=Z0|;%JtKm27&8OUgjXF!!28Jzxo?tt9OE&>n#BF8UZ9JJy zJxQt=v}#xzR)^5-RCAei23um;hHjpgis+5;)!1O=j;ejZ-#BjB#!;6L?F(xVanq?+$(%HMNeRM^V-pT>j!oVtwwc zUah%it)3{TVL!4ONcdgXu^St|(+O1Jloy7_T1LB;gJBc##+DxL2CmIIO$*?DPw%$& zyb4`^KFJEmf)I4qdyCB1i2fBdnQAAvAQjd*bjmK-6%pDn`;4UOD7J7Yp{qO^T%J7E zTBe<^v73wSWuUW<`LZ-rT65p+#KTT6<)OH!PkwI2&iICE3L??eYm0WDVMu6R@3vj- zdbCrkG{B29j#a1JVNgl1?SNS|>t%kn4<^&zT|WZ*K_dpAdomu|VhDXczYvTr zqn8*Q=r`x7Hd7iTaviMJgbr0yaQ!?{0svtgc`j)he;DX%W~#*$G|DwWDh4zjMdmD= z3ig|iN_1mj6Fwq8W9EiOJ&zC12^(icR3~2#CCVlGupj|EY8&vZ8ii%YOJXCSJ=!P) zWyl--P<%c!Ic|8^^WcqnQ)3W|Yw=6|MU6GXL^8$CDlrRK0ccNiQx#Z5_1i36rR2jx zF9D>}!IPPNB>s=@-dv8+r%EXOakK+L?!Stf>SJ$~UILx~UuqgtnDYI!cbxFK?EZy)PFIQB02Nn(=zOPm~ViZh%{u zu+ezj!+Up%>UnN_Qr&9fg0oTRIu}Ep?kxruA>O&_h2Mq~EAMwo*sQofh~4UT_G#{t z7Dy&lIl&$KnS_1QSg6ION7L3rF_L6lJ8R$-?{bc3j91(?J6!#EKZR2cfZI#VdT>iq+2LZfj>7MAZI+kJsR za`E14(>oeHD@E`R(;%1-$0i(r7pw1PwH70J-B13P=%&7vu!c&cIqD%ca7+HGnOT)7$t3q9E;f{ zADZy<4EO@oTcvHgEyr!9`c+u_(&g?F69psy7vC5ed{5n=Rpt0GiaOFJiG_b|PU(A5 zS(%~_XkC{3`FNmSucopd+RmjL=nq=nM4Y>L%qa-ko`4s?@ zhz@%zeRD2wV4oxc6g5;!&Z^q=e)NsT2-<6CTw}^yx)N=4+Ao8&w8RRg;jJPW%8=Xq z({`Ajs;9!v=}A8 zK{Lfs>LTuJ2k26*KueeW{0dZ9Bkkmvik+Q3V&iSAi5|=*(57AN@Qk`W;GkUYiuKof z@Q9b&`OyvE%cOK8XHCP+ztIU(kzqG|!ynqd5Bbm7{;4SZ{b~IICV9+w1xNu)YL{D* zQ&Fy`789RxT44T&QCUJqZBVI}ol!>LeU={tRMT@m&X|r%MIaqpQd(-#)ZPjD8W}M8 zUgn}b6E!-82dbIj;jEpa;;N?J$DnxkP4&!z(;JYZen?JE-tysNoAH!FQA@4+CV}BM z+-89@#ucXJ2k5VRJ0lD;2-tWO+9DRs@tBh%Vq=3a7gULcB+n?h z`Q5_<+@Nps3gKmHaJ=|ECf-|0hdu_ho&pFZKHt0B(<-wWtUbm(N`g0VZzYf9_`RED zvNGvgf=>@6+MI5458p^|XT-gFfGw|7A_1&=2_ZNUwV2XT7Vi$b;agnHvcSb|zCORf zubd?#>k*HKlgm6ci_W&ThMX=Ddi}ALUr7m}wwXHKvziJu4IT2A&fV?72tfol6;-*I z{jE_|UhXXDejLb>H8b}xnk<&gYLI8P@;qEsNg8+)s4Q%E(o;-$gznVRj#Gb>o9?JR zU^DVOL3X#?3L?)0Er^6u*`1JKXnC8WF`Em*bC78uGfuv9#_9A z!8w@=3o$ha5elYD;F>sDi>Te+G1jiI7NVk5AEQImYVFKhmU@sOZ*&d6P(1Z#FYuK$ zeCk7jti^073O1<7dpDkci>a?=j~~})7O34#e%G9dG4Q$Nds#urOlTRA0FX~Qs0i`!6H^D%MsxcN48stJCJzIIdKz@Cg<` z+2F5e%locFN6_#-JgOc%u(nUr|O7a7dm%MOT1l0UOs$TBY|&`OG{17 z*uxN1z6*Ax30ON<4sDJdc^_ak;x^tq?SPcXxXO)9glsL*u^YZ!#5Mk|o1XI712%wk z@+HD13Z=y+BE9hL)OiVcb{%knfnk8wiVbTRprTufFu;#rK4bMtB}(u>-}gTOE5^@6 z!~7=Qm%vq9LhDq=YTk>}0@~K*X>%n7lXe>e9h&rB023f{hRw8N zCZLOX960L7x23Ue_2YtSaWvuSyEa7qIu#D3WgZpfr2Qb@pCD2QU;v9c|T&7|LK3OE(0 z@7HwuucZr;zIlZRU+LvJD$3;S5UhanuVhYn43?iP2j)2iYne=$Wf(2yt{|*{`Qlr? zli1HMpHkA1NL9Uvu`54*UyKqn5BH&uIHudw|9@tqv1I>&)O%O9G_2is!Q;Mv-K8u? zX#&2#M1%a&R&vvP$i-QWiJ_~4ZnBctP;gxa1seAntGwF)O`*6cIE&0D7mm6Zn#Fral`H>f^e1371 zSP!L)w*@_09V$)OT5(EVKv-G!Ov*WjtLJINwXOEANLMYjDj;fQ`*y^CUEqa|7R$9X z_~FJ6YaXZ=pweaDnS6OE{%wA<1+*n&VP$1Ku^_GgjhF-2f5bQbTJ#9N%S8CZw?L{> z<*7G5H{XF_Z;qGyi@^w0O-wg+5mar)AxOkA-)gKYF=|elUHt-!r#4(&ZxRzXwGO|( zGsqpWzuDyvX?rhXYsU##Ax3bgJ_7^ENjEAU&g1i_ z2Qs8XK*>b5&G;M+BAw^q1w+258j1s95KOv0|97(y5WV^BU&U-#k0B~bZEfu*8XB?$ ztSBip1MFa+yC~nOZ}#Ks>v!!;TctP@btLQ7wS9lbwAC&U1)*5;5G}Z21G(N1HM~F4kLCTEHVBv8ohz}}aaW^KsHj|bs8+S_P zlWSTEYgTXO=H{A;5GS`M3Mq&kEIaM3XQwb-X^HYiuP~mctAs}RT^$4e@HPPN za9+FCILNwUC`&`HM0co#?P6cmW<7Ixb7I4`94`0MEH7)>4xxF)mex=C&owl$iB=j@ zCrnmBiJMgX(AfK=zoeiKtIh~yqP!0(3fYJ3HHsvY>i73);zZC^+Ep7|7JZKFMh?IH zlIXZl$z1ijCB^3BFD?qL^hTJhPcbz;DVjcD@QI{F(l1wl?(VzK&&!!44K~N!o#x&9 z<aG`$EYc37?&q=r5WZf!o#q(;%|=8PN~qfju8?&i$N+?hFxZ z9H2I5E?ON3Y4o@!Z3)AM9Tj+?KW?0l=P@_^vTatso!If&tG+nCFE8Auxx!{s`XFV2 zm&U65WZGjUoJCrEdv9gbonce2eDNqaE+y`S*VFiicd@Ig}Yh?~oL+BSbB} zl)l>}Qyh;=yteDfV)ra%)#`9wJ4i)P7_92gwllacd~s~!7!YZ$(9GAX(V#Ei*9vAh zc_~`*`Aq`rnB(|1wpIsSz;xFiHLL+lteOd_$a-!pY=L*SYe)wxa>dczfpcn)l8Rs@>1 z@=Li!4Qc4QBpQAJ7$zHW*0gPe!}pBYR`#L@1D_)$O&O(OG8xn&sEJKdb%aGWc8bvt)rGVMI-?!e{psg!dFd}?9mS68ovgq9Bm(T{r%f66i37|?fm&1cnr$8z*2IMHXg<;k;W*IWvg zHs@%_GIpN-RwCQ)&n*-QP5N^=U~Z>R9xO$Z%#qH zGdude51Q~Pn)-PMM^w9p&l}`;P11BA!YfubFjo}*gTawew?=-)_PcV!@8&iGXwJ8c z*ujy(i|_UdV76St+0#d8apxuxmth6^$`ejAZL?7>B$CH>eXwhi$5b)RiPKxVi1_RF z$9mJJgFVIlkg6~H%BzbYml{*rQU8PB!lE#7gSkH?;=R2JHP#yb-DRwRTo4soGUAu!_^;}NQI&1V7%kF_Ldj+WxB9LY-QrMQ zyKyg}la~tZRV#OGw&*{k;W==-=!O)1%*(I;kF&Q9 zt9skMhmjO11tlev5=lvEM3e?W=~NmC>DZ(JBHazr(p}Ob-60*)-LZkqhIehxJ>Pro zecayP^Zs)lkI#0;`m8nAoMVnThRtzG!cMjZsUsPWPBm~5Yp1N7y?yKZ8!V1t%|r*~ zaB#8=wfLXhE`1rprUQxIDS}jPyWX~N2Q_~7TG5j?pKwXvJRW{vUB4#4zy77UCnjpe zWxJYWsUAkn+jS-m&t{}{su)FS;TfG-WDz}S)Xy#JPwL$#%#9e%fEPNxrJ0Fd+KxVQ zxCI87R81~sPkVW$JSKZmGyFHDyO?wackZvy*TZVR_5k-Qi^y+hjTQy$z2MU{bJLaD zSqin(?l=1gGZ`HCcb8mXaoNm2?2uCA>pE-0*B?V8KOA6r8xhedN5C4HzUk)wU1@g_@x2_W&XQA>u ze_*R|S-;#;k8j%Ay`VbcAH_Vxq z%1IDj(HseNAp~zTR#4Dr_otSTXYO#!)*)bSXu)+bAA0-wv)4C9EOL?GSraMO=OG3ox?)B>QKE2EPlo5-EXv+KHnPo_kChO#}@Q^h#lm z!`V;fj78r@n=+2naBaC?nLgylc$;xleaW?UsCwndfs;h!{HOd1eJSR>8Rr8^7q9?N z-m#I>>?v&1`0>4KhL_AxO1)`F7!kR;eq+b}^wwx+8?_=|{H*7!juL&)ra@H~@miNB z9_nhkyXYpNW*qIV!teJdmm+6lEts;^TAkeHQlJes_y71*A9FEe;Akg_+!Vj z0ZY{aZL>%^!4~YIT5g+Ki{h5$nQ5aCl_c}QbiVGBI_Q*D3EgJkJP?%k@l70<$8#HR zJ6#C=V5?mDygL>1UXo`)W5>5QqO@PVVS;0@-*+_|JvC@zfnNQav2!UcPZW7Tlgcgt z#8#U~PCCv#tialF;~bb6dp87;e9j-RY z*=a*Y=e`}b50X~TuIjHAjvuLQ38>tX>UXGvTqY0ciAC3i*T{u~jk8s*)`_iiA7W7j zMbrp@nuTV(Iv}|nJ?*%$s%yDiUfHxdfMHypqqjIIxYo7iazNbv?TP=&Wwk!-x2V);+pv}vThiGKo6}e11tc?W{Vy<=hKW%+h<>`8hQBIv8d%9%)T5W zip*%(be~FoJ$kXFeza*5rtRTkG~y$8^4XgVFJ#!>mf8Sh?3s)oV5HkH`=cMxe#d&* z=4bm|g^RTZ?7X?t-o#E|{?UHgexY22^6l@d%8T${WjaN4amhMZ#y8si$^ioyku*deWc_#uxh7Xu;)IW@fugk zP`tIAy{k!#_rpsI7O%N_MU}|l62)@+HBSC|S%T8BQWL)F^O8<|A3-mrAx?wi;?v(E zH8imZz-7*AK7R*;>CTdi`39|5v3?WvU_QjCKlLj~NgU@w;&#;n)xe;#Ky?>!H5Uir zjmCSdtUNslI;xQdFb9$Dyp3Tq zc3{2PX%UmdOC>UI=CYnWU|cTP-`{@)7bf_^VY;~0sW^eEvew=f+Uk5+H`=lTZJWrL zC=SHnn1XgpexHK;2L(5fR z@Q_dP8cuSy;|55hCb7xu1YN>T3%)*njK#6J^SOz($Lgbr`AA-@X}DA3lrST>$n-SJ z&B^-iq*jcpqcpoGTQzJM^Dlu5%!s?0vJ#A3L!IAI z;!!qMElW#cpw}@Z=3Em$-)j@EKB{PHwN_+awcD<~G^t>}N6Pq^%4Mn>x@A|N;NfZw z?n^HBGu$=L@gK$~MUM%hfJ^4UczOyNsX^<_PJ_q3@f+DW7BL0dWv0b)u>$grm2nj- zi-v-uJ63U9Y^+NbyL|z?g4C9YvRThvp%G{Vo9BJxH=IxwIv9OB9VZ>%jI0H~M9hW{ zTd<#~98WyFeCxq!(i?5;>Gb#wDydRB%Wqq8Mc#<$&#kZY*RaSkWoq9_cm(i?(O8JA zBc1|iIy=6@piAj?a`FBrTN63Gyw`Rn5XcTrHy$0ushB$kN5ouj&2}U(5_XU%sLyGz z-Iq;n}S@jr47nCyRIF66>%bAaU*)V}!YAgE-L zk)AFQB=9vic9dje0?S|}(TTh3m>SRHaL2Q}qrSfwhzDs`OgmCQ9NtBTXcnhR1c?~K zot$kK4OHzpN2^5=HEJC559>~B5%oKl$j|kze3MOQlX(BL!{|QIWXmY_4bS4k^&dSN zSzKMwg(1%s1+_MPwq`adw%StZE9}1>PcSdNXNweO>24JDoPw@>NyCrcD6DaPXAxR_ z`ec7Kt+|^D>vq2e<}6P;Kl&A1UnIojSJ&ud|2<+4=7@;dvjx1A=71{9%WFi#{Uo0p zpEXl7j(OCq>gl%)TYP_aMAq0J-GydNN_U3i1TmlFy^G+)T9i5nZqUPI21=9=iLtJw zh$OD(3&*jSHD8Lsm9l@}zZ5E&%&{^YeFAt`0h?t*_{%OQ79%7KA^IkdSDzl;0Pnh< zm26XRVCxRss*KbXu92o=XPH%xy}u*pF^jEh8X^v{MkIGZ@ipwe_~wp2ylA7B54iWO33QN_-0)tjWwW> zj6BYBr{cM@GW|2!#@z|@(b~2jO7pJxJSGs9su**#JGF0>LnOZPeYd7*RHJ+_M_rEv znl}sEM7OKb=qJ}Zif26%6D$Y-nPSYwg#Y5iz;NSu%kbj-l=F0&U=ipU*%qXHc@uHV zNGF*IbVE{J{?9nyR*F# z77S->U>qgL+Z0y%@t|xEaqCNiRMw)%uZG08u0*dvM727b>NxegUGgEhb?qEEFg%4P z!tH%fp5DazjfOZVF)e5i5U4Mes}#5`j64D?PQ8pI!iJ0_iyR0oqbH4m0 zdmM6*O-2A9w1xy%^TIEL(!H1?-6VS5p*ZYchjCSY5H#s*o2>Tx%2jy7NCgV%W6KtU z6=v!Q<6hc8g$bU*HD@eL;R6BwL<$dk*EarDW2jwl>Q&l6U-!#hts!{_Te8n%!GfLq za5vA+nz!lGaAX&x&)eMMIeB9`5L1qjC0Te}L4HIYDcD44^44nKG8lx9pU zdZfWV-HH$V=iPfOKJrvIBpXv|drqIR;5%WqmoBu)Y>ec;q7*ME>(n)7lEkB&v%V@;v8{l%Sx%8~RL_S5(yg}d?l z!_ThZPRhkoy%zObVpeTmj<>u&K5i*ND<5b!2s;N8FI@nzH7f1leb96Te&qI8F?TdC zRL|o=<)Q?YHC~;4l1C~miRvS)q^$T%Ja^8)JV)=YD!X4^US1W{l7+%NL<~*fY<4Mg zm_)UnJu!=8T9sIFJuW(l9c%tv8)3VrPHw2NP}sS8$m%U!oFE2}iR5`wW`$*jH0@y^lBYqa1d&C^1{b zs~eHv?eD+b4&>b!dC*hV-+{0CBFGH$>5nS`iH=hV9Nj5vII3ZVwTSpLv zxJX%5+?s!V!}(Z~q#Hmj3XHmvkzzoTJo2yZ4oWku|9|(%?|3OGDcw;Ng_pf=ehF;yt|K%>z`umo>wmcSA~#=rMg{re z)AX;R%c0QHKPl0&99%8PXCOK3=IV>V#RZx$&t;^g{f_GEzp;SvYu(ohYv~8aqLf3P z$0eCIV_j$C_3n^T#piG?&zF9Yams93ynQ8ig_!;)83REIDl^PD5}9Pok3S(e*rFnh z2+5gm()mwPhl)iD2a=W~q7}$qWNevzUhP(%&LR$yKre}3z#;3Q9y}g6vI088ga$m3dKHWOwL<_=e?F<_7u*8Z)?;F=X zq?eUN5Gv0|%KhSpB;KQ4`tcGet>bg?)v=|tLsw19So-Yji&k5+@~`-K`hddrJTHj~ zDB3nv9rpbwF+Z9$eEGfT<<;LIkchwgY;$#NoFta4$I!orlf3Q8kNx{)JI7c&Q|Rwb^d-KTFw$_7~1YQ)!Fb>TS>=i&%^ zS;EBuREK^^;C$K6oWQGdps-v|1k|j=zUMiU9~_Xzw5xOyVi4WD^Vj)i6Zwo|!#OlJ zJFn3#YUA1T52Q)P6DlK2I7;7Bky2FS#Q7meXqgESRyp2407zvp&-flX7w_9Pq+)2^ zp7fs|`8#A7l{i{v6IJz>D8*an#FBLUxBxRVfGGHc3(?UBJVYV>2_gfR4$Yx4!KAJ6 z-zJ%Vt)j$L^n6>TlxJef@Op{c^^bzH$q?$LBT$^!bW~0b^EZOrA;soB0;dqm*mKJ# zwVhqe3EctYw|(T1uH^NxFJ8u)CvHrccb|QZoi)@x3Fml|avhKiQ^`olfey#;Q))ck zf6h((*S7IKyAFo`fbt!P0J(vWy{Jf!ynFeP^Zms17l3TZeV{b=i$}}b+d_=|`OrM0 zM!Xp00Ni^D^70vePVS%W@R6UR>tA#ck^+Ya5k5ox7m6*Ke|jweu*Q90+)XMY^#hA{ zPM>!R35Sn1j%?Ck6T9i_zd;0UQhY z+JpUlIU;Kv1;9Zl7Gdt*20TP|c6O&$hjFSQ8%T9tUblrw;p^FY_uJ9m82+$cK>pFI zm2`@XR6qvhT^16L(O(aTT>CR{1ez!_1H_VFVm+7*wA#OGB>nw&A9?qJB<`kyb_d~2 zGE$+NfA|*tpMgwVk*pLT&&Bc*qjQiSM!m89!;^S+1y3IcF2xWkdo&~iX+xYpyw=ni zVQ8F~$eQvD$cgwgP-_24cEA)nJ2mw#80+RCBL!<~sK2ZJTO8vrhLu<|4iJp^J06*y zXnni&hgW-1BDR@0CT1TR^2_2Xca;|qaQk~mp8UMv&6Pi_98H`z&>8?H5VD|#z}DZD z`@`ppHUF*WC2{x0z+ZY^X)mp@O_1e}i02MmI}tb?0WO|tdPGEodh)HOZ9p?Z(@usl zvGQ^)=AtyZtFsg6!&%SP*NyxBNh{M!tQK{fP?;P;<8`C(&rD}=bnZu~U%p8Fiz24{ z{5iHhokZL_q*)+f&6eSHj^}mResH8Ss#drR2Qs`cYff)!CT4rW{LS^j8<)42zC)XKsySox-Dz>Kj)`DAg)Ho9?ljM)$ITnq6+$!XgzV%5O1H`9qU z*ujTePO}~RqG}IkS+1Kh0K#B+>1L8&o73Ry!mjESoy8e>t9rA~+HWqZl?;B5x`VT& zf)FZ0Xd45~O0^G*RO~FdI3%`=@rleEd=rUsJsUP}^(5W7Bp zSxop&=9dS=sTjS24Z*bnYdj`JIyDvh7d6)WUE8tFZYw?U>=1`fy%jM6OTr!H%5^bF z;tdc7{ik#d<<#yC#ZK1Cn*6WwIoePlL(u;Hi*nYs>i2`Ub=$pBwwq?#|*qOlM6mcBG8Kz8UAh4#K!jHf;sb_P)$Xx$K; zE>BvgQ4Ert{;dVjO6{@gu{M+cdN`);a~+0p-{~MX#^rX+PKL_?E8VLf#{&A7y(M6{ zP+CUjQ(gAK>44<(#UsAa=(F)x2R(=x;jaCqE~BZFmBnyHz^W7_?gRsgcd)3c8tjFO z{VhNXtoK6T0MPIZPuf?*)YY^dv$L6a%?3C2M<&CUMrS=rqqx!+*lMH-b*K3>ubF~j zAx?+&w&fbP2at+97$os1WT_p{$=Dd}`Fn*dlt^wEyae(Wi~i$ww+UE9%NBwryYMHi z850GzSSM^|k`C1_DrX<4cQSYK-P`%`GTw6W^E(2Lcq;ed(30TtLZ_TSc-v5>WTehD zW6Y)XT((P~B`(yPd4~7%ge(35g^XhOL*{JbFfI1QCC!#brcjB_E^h-(A(kG5gXly-CzRUpO?Dd(!zJiS#5r+# zf_5+X*jeatW0$aEp}|Q}lWj*!`E;|f4y05_^kr(mZ64lc5s28km*jGwzl^hWr+RI9 z(r|6CFwxSJ8IOWD#IG+{^p2hHi=?^>n)}!kMQMM*slNsZ9 ze1ZnskwAy4J;03p6c62OH^bc*@CSQc{o}1Typv%QWp=IhPZ}7H%A&)MD*Kw5f1eE` z2zUcg6d7^xx8GY@VyXQ%f+w)`Pxj#EYn)~_2U{YLGNwx2m0t6KyugObi^*6*5(;M8 zWb$nb(ClHTE^f&u>y+mw5@NQ~Rorv&+QQQ}<;aj(>fGOBOm+r_b-bgn3VMgA$ zutoh*6fx)P;{*e&Q;m2&6ICCt&Rs@HO@cwaT`s|e{>CfrOHGcJ?v4o`eKB7f%Rw~m ztK{6fr1!G9yOTlUA_Pga8~AX^rGfI*ioFd--?T!0XqnH5e7CsIpn-v2F8ZrOFi$(+ zP=ri5w&}FtNsP7iB}Fb8rYwTNg1X$ol9J&1p8W>2L@v8l=HoVwLh?gMm4RCFgPqnE zlF?561$_O4yap(I?DuK7_j5)d=d8X_?dt4Y#m{sFSG2-UnrSd?WkCcolfb8S7(K^W zT2`MG=vH*njGNW3*JXXS6pgzeP~_NiZ6$gYXd2C;o#Wz@>l@uIlkJn(C28EXlk&L( zH@aYEr(`Bdr%yhIzpZyYGAA_r!7#KhGPBg&g0uAYpwG!?r@Ano31boWye9P#i6~{T?y+*?{>?kr1s`p+*=h4AIq+qGEqWP2^NWr>+i?u6q)qtu8mda)*_n(o)Az+vYp97A={Q4L^0J{6>dfTOi z3W;4y)h5YN^DUoZZMN`Iyzf9$jk?PcV(Svt8D_vLFJN#SdbpT>=NK}1vJ=@xwXpo~(<%9;ue4l`)DrLETil&d*S{TRU8!B0IdiHv{LKf(Zg>M_IuDG4eGa9bUoa# zEm5`M?C3e~{ZU!b=rDTOPf-Rr&nxSNf7F_LNubeh8}0d0cTDH+I1%_!qVKJwz=d*N zWN&tlVoj=zTCZAce5x#b;)qImF|+Yv?Df!U_H*6c$v4w0sX7!BS}loBJ~!o#nlQ@B z*_Xv;ILvUHrpbS!O}m@%>_x;)X2+zC&;8waJv49FFV0E=oXsPIU{ew%ae`hM@2c;3 z&9PZ#j4l=m4GV*;H@w+*)oN|9yrvtNO=?tLI=|Dv!oM9N%T_bkrnd4Gjg)c0x`Dm$ z_vKay7b<`}EQOf^OOpK83D!WwwAmi1%c?z|ut*l3b+1vrcI*Bkm*ZWEiOtiRoe_Rt zZ8sa%A^l0R6Q>Nl%SZeB%tLNIqn?$TWqDx@D6Of`8D950Q&`*$d&C1zTaHuP*7@!C zNE-Wnzn>M2)tSCqHE9)Hp|a(?at`loXiJ+;7xP9HIdIyUVO>a0{K-{u*wvSAzowjd zT0Z%>%g!v8Z6}q!5&#y7b0_;t8Jgt=c(tWOjqOYjPxnU&I6a0LqXzDV#^TC8b%4qq zyQZR)uX!u`CC?0-QmTXxe_SZ%%XvBz?S@gdL_vYcLIhrXa>Crx2q@A^(pXPJz9^%|y+dJkySO=jsprKEC z{P!iM_j5M==#W9&054D?kY2n$y7EQjsssDB@8|8k%{*$61cu@zw%ynXaQBJe)o&$U z$SkqAUQ#|+?a)IpILawpnzHLFCg3X!hOV&>Wl?v1c_YekOw#CVwoW%-feM}9A=z^r z`GynD&=r!R_%+J}q7!d0nx{nj&Nr%vT-aQqqR&84NEbA1x8O#iW22Gd8z#xDp_5u? znEvNIKa{nFsof>WxgSQYg=tU|c(y^>crDLoWU8-mHk$xhkBlK=&)otjVN3jx0Tu^0 zbB2XnZQpsOd_gn`<0)pF{kJI>Yne8M`oQ|>%dI4^3>ZbfaQZcVT#1H?2CK)!5xi($4DuZ}N=VfVD z?5HC_*@LAJH_ZIN#R!c-xY8`=BS$nF&np2uvu>;_33j%Clih#{b1k{1kLzihK1)S* zT&z*205QtZww1Qbez7E;Y-Tw*#rQ6n^e0zTVju`f3YG4T1sXfySX7%ohj3_17s^i0 zygc0HPC2cK#f{reD4a~LC$6D9UFwXK(_q|jFeYx?C3#=+j$+2Hw)`ihOA)+Tq#4jy zw@(T|VrYacI#r7yGLY6*nWncG@*3_kQm1!xtJf*aCN>$8g*AOv3~~(v;mnT7i6gCu zgmB{0{1mqaBKsmwdpfJ~NvW~C;h@AUp}gzviAi#F*3+Op{yskDXz78}l{3~m4h+Bu zEpIbuMm5S8LdJXOXK{2j+k~fj#DhSWpa*3@E`&%Y2%NS9msw`Utx^b_TwTK=vQ4x0 z#6aWtXmo!_k%CM3FcHH#W_fi>&_3++faKu;cWI_|a$Dk7x2pK#$4etxnlYH00*r+q zh{x461$N5>0o|G~Tymio1UAG#2+rx8#B=3+-tTb}v7oC}@y@TG13Q>l4qyU_Mz+0F ztSg+CgjV0MT>*uz35w|^@;1+K7dHFiML~9|C-Wel!i5CMYHXLC>EEgd_Uj94&YxR# zG@dlN?iMTsOm&$gvqXVP0{Sd!<3gxj_w2*k(M#$t<1C{(vvL)dlg0$np3D?;+p10i zlj9(KK?)N}oY*$e)egPSR)8anAA9XR*=Ua~JNrJWV5!Q2#HEfKv`f`1dT)-+-a&8a zdB)~9hT4UchmoL>=5N<4ikz<+K-;eG2xl=F<5BP&t?{H`o#5!7B(zVo2Q@%RE081y z!`A?+3)J*)+;~I~kY`F#3oj@u?L2oTVbvMRkdIIWc*t6%4cO zU?g@Vv%BUgUtGR-L_7WUp>^?Y9dzk}mp}kmy@BMN(|tnLn57b(qvG;3OZ&wFVe&@p z(sKM_Y*t%LeCN#GS%Qmd=2KB7S1YC5Zj~EM%IP21U`m;CW_v4pZ+wqcpEemB&&?N7 zw+F1T%femN1nq$Qm!*#|Gkw!|A@juybJnK&%vkB(k3pkMr}pjP>3wKWgVE&$oYQUW zx{@G7qjXuzY`Cf#5gP-BKVN|_f>EFlpgz@TGW#V|u~WxsW{xXU_}OxIEOU!L3E%QM z{M&nzM1IXz(z2}8(Rdpp>X%v9x2ZTc#`lg!)mn6-^5iFv_qV@66YP=!f+E~M>*T!G zQf|1uD9s=!omQOoG`OdqTQ|5ITw5E3E3ba9SK^&O)Q@Q7n-cAO{!GKk!I?Jl6~;GX zFYCh+(7g<4!#dod-J%vcE^+~dpmOQ)(u`SVnPR>s&l9ve;kpT7FE6zYRyv=MWJ3Ge zS;6>my9XSrqpMKvpjsENq72AGl^G6<>J?tM^x4+!jAmz~_B^S_wl!Q%kDML1~H8j&8Qn zhH|pAECRHrvuyvlp$uKk!MGR#CQR< zm8RY!{Q&T0F3qWFvv!YYu*g@%3LF_IDx$7^2 zKB8BPrQTCJ-BsErA8|u5(3ZfcdgiMGGuZ>Tk6es=7#tp#Z~~w``7~C9+mUUeobw{K zSY#X%+~fL3Fw(8K(4!pVJEpvEA#20c@!hd7A4t1~=Tb(;V9`soQunW$iF`oxQ?$@y zdy_3gCNctn8G&TO5MKjOXs7J-CO4_9mffqIQveiq+WP?8uVb)O&DvzLF7*)~Uj6uA zzy5KdB-*RAHX%#BxLGyU+#?|FcDh-LMk?T30)n(LvLGGng~<{Y7DhL*o{~pK>h0}~ z&JmPVW4EHPw)=+U^dwv{nD58t=%558m21DfG}KAVk@~ZFG8WvZz9vu63b^2!z`$(X z;om`j}nGMv75X5*44o;|5=c4Ze0tR;|-IOxC`PS>HNCXq`gqj7MR& zF3&w){h#`=JP-xr=Lt4Fz3LpEN#EO|om%ae5}in7Cb6jY$+|2Ij`Qe}1Fj`z)jiRi zvh|&9OfY`c?GEQzu^-XB`4*b`aRY7)RFesNOM*CT+W zL?#EFK7g~0jj2Chk|aWJZ*0%p>Ws`aoKNrI^t^bBD`|=yKT&Qj4Yd8}Mr5q{XG+{o zuaR9&p}XwVJt50nM_Vk=5jZ_`DPok57a3Y-8khxM2CHdT zv;LxDkc!HI``C1i0g8Y;mga(VrXSD4=^M7sHwqi*nfO~G!Y687{QKOd`)w5nG#b_$ z0E)o1S&9WTPuPeU6!zD`^>=c0>*5YQYO}Q6RdgWtnm1-WcS2^|VJ0V5RyAA1F0Ogj ztzDkw(9vLn=?N8vNTy~CBLlH612a7=&vD^1cj2*u{IYr&8{@$@NGIN`Cp(uhUFUGL z?%p=%B)LS*Wj#a!rgx%+Pc6dIrwRScm)7K7IMznl=d?AkBX?i{sjoMmC1z#Hl!f6Z zy46#_6=pR;9=vYVDv*Ko2th4c190#X+;^LN_s7HNQ;kwm^8p;JZBxx%$qA-kHxF>} zmZz`a+C3Uc{xfoc=t5r!Xm6L2k%E(qfZ3Cc(F@N{QO~a7;Dp;Q&2#otJe)Z_ir#fi z0;ras$itu@w%6q0w|AwcSQ9D zdeVM~c3$(C!u$14)3zYW);f@sD|fHMQr1+)H4foFv|%O*kZu0VJtyO~3FGm-o?b|D zcLEv~{z)C*ImRitr>i4aGzNDj6Z0Bo!#O|x-NydUwBb^k=D~vj;3V|%BS&8pxbzhR zZUcf<)^jl~23=400*k4W((~Pg;7n*|jdcSPP&9L3dyJ;v6IXe?N1+%X2=vDyy1&1W zX`HZsxeqL~htfubbn564r~$2o3-pn^Q&n;vtE89FQQ3q0pfypcFypZ@jSJhcaH$EP z;mQl{0JEjR3)VMi`@ zZfXbozDp_Ai_ioM_+g@{g@cZP0W3)l7HdHZJK@@uEw#P#_kNkDr+d9lD$~qH-#?}h zzJ@1LBJp}z#6#HC02F8KK$ak&cCXGR5@F%d7%X~0r^l|x_By{rX*N%jRrKO8{O0`^ zQu7O4vfA2tQ-kvi*}8S)NHmi|d=a)yK;HmZAP60{*ku2HH4shAMH3KDKtBz*9+7YY zhB{xb3349>n&7MHpd?ru%=iEt78Y?9IDh}(03Yl6Lke_4A|iTTUe&vI?*b+wt)YGb zo}NH5QmqaY?7gvp{!*&2&;W80H*ZRyEU=u; zoc_TyZqeKu44pGQ&kT@w^2Cq#2$+hgdFp82{(YE5?D11QpcMw@<{21#7-mi{HCEZ+pWH9@3&mc&O6_R;2+j<37@_hhv}QRRdHXzs5n&bp znjI37lCtXRG3c0>VcwyP_55tei8S7BY{2YN;^|WvO-=GE0d5!W2Ydg$g#WI9+mBEs zgQ+6lzeo4;_va!bee~!NI)^R1vK0gQPkuv;@Vf-h_giywPgVw2tHy4|!42daqJ0Y$ z9OA9n>!Zkx>KZi|KFimJhlk4(S@$$v#mCQlKdj+Muxs;&w~a)=AC#1=fJe!_*Bd!| zo#eysBNaFVjT-mGwRpPYMzd>tS9 zV}K1n2NvZTG0VL>ckX~q-vULYru=;d2j0kAoAxJ|+mo+D0|-fecR<=Q>U81P0wVhX z2Uv2@nkYn&m9=MlWbfp}*$($l&S5CtoBu3dHd7xEApOuQ=(eh$<^D^W^xfOHAcxSf z>irYVVf;ov73qTq4+wAFGRhHC62C8K-u?T${?})E-zMs>xFszuO-M;;$;p9=hSt&8 z{=iZJ`C6l(j{i$cGB7Z*zyF!5zM*WOH1e=T6N2Et$)J$}^*^wyY%FIGG3VbK`al0v zLLU+nV;biyDIp>C<_!mZdPznr9rBl7Z&v~ywt(bwo8R_5%F`foyFXE{9v1w?bO6ph zHyR%nkP;#N8rQ{smLbf{wV`?;byo=Q=&`v;rsa6ee z;_Np~7#fsGuaEIS>FzOpm|LARX;q^^t~YycRGWeXxuc60f+7LVB|Qg62#Qa`4ic%EY-JunZ6?Pk)NAgx7KXON7x;8GP)LhCSqf@Tg+DJ`6|2jdy z)RG81A{(^B?IFEP`Q+G^0E3!+@$2tUT8nr5d5|rw_Y~+`087vaWBpaakhnPWKx!U} zZ8aUhvG}_B6$r)wjzwZr)Uw5fvzuEjKSSZg>2{5S?UsXlLRX=7by!*TrEa!e%TkH; zrN`;{0sVeA=<)W6UUJ&?3Ko9JJ2{gL@14IL(WQD-f3bx9eHYR#+D;P^7Irs7Hkud! zTc4$=`;$oOs5TZhb?PnrBJL@D8y|^8v_S(|tsGiGsw$tYnx9i@S!gj+`(&g5uE6Vd zS@-_K2Whv{ecRKeXz4d^LWEmtsQ@^*!{*jUR&$o~RrQnH`}6&c!u=K!z0Tt;Pnni$ zw-(oPH)N!wK63Tdq#mewmOkaZsO>llla-$S>X1I>vLX$jW?25uZl4{`>v%s`VUa8v z-_R{(BJZ!cQw+?|h+dQp1hlnE#P=2Kz~_9ayLuA^GmfU6I`~%-!si}hBHwrIav(=j zdSzfhfA{V;It8Hn7D_I>Y(@q>bT+YHi%yQ!_u|seH$CnU++i!z9g<|PTYSZ&d8lpj zDe}i%I2L}?sBZjLz&JDcby5+FBJI&#O^DR-<~Z}M`zm%7vwBsHw;K#QNP1%mOe-X9 z2j#L~6u<4OI^HDZH_sCiox@1BnU;z<%@N{%^&$$))_IB8?#PJxO z?%(nt7^P{AmK6zoi-98mtFhl=E7eQhu&7bv4u2dyT?36#vnJY!){A9;pAm4vIAMqu zV0gqLI`ev@u2VN}{$6+dqcf;?3%IBVS;YJ7X#+pnn$ z`gscdGZa9DxKyRU>|J^-Zx?@Zfe@Ef*_8R#kP#5G94vFeDJ}3ktktQp592hF>B6FR zyH{%5XLG#P2~aDkV9A=;V$q8+77{(pw(&ymYtx0}bmZ-1Jgy0))<3=K_w{Qkj3>dE z8l$OMo`9ORv50I9)^^`B$F8I{B+Xnz3Uz@FSbX^=lSXv+?{~fszl+u!Ja~pT+qLC1 zwN#lNrZ9@@H{vwQ)Dy@1;L26l%?9{!jkc?c4&*t5yqiKCkKo94?RT^eg;qZeS%i3A z#Y|1F_ks}+Jl$IOh)~vSOB~OvOj_&OIJxWoF@XpT+F2HKZS`MB#X-`{rf<_@pRRP0 ztcS4aD*?;jhVK8Rov) zz$_~}tBPrRs^BXi&V}K;*0!)dGt}3H=nxH+X*sP!W^B8PpZT`~>G)%?Rww zpQP@!a3et!HW6b-(VXIVO%P0TXFb<&IY5omP+u3VH86mROM#eGb5pK@dXvEQ+q&n2CG|@j6LAJ+ z&5AYn2OKb1O$b`KYI+WNHzTPoq@cPA?6`tndtfQ0mU2k=O`P~JoiOtw0WpInH-BTK z1z+Epi>teL;oRkDiOF`WeDr$b!yuv>q}iu}94HY0Sy0N}z6@Llv5uJN9NIpJRC-)> zXafz4Aa0lX}XV@GCsxLd(rzwz=tnoyN>&pk$3xBp4udB{&){(9HN` zYQ_S3;_74nne+VPHh74EaJDiuOwSgRKxuxr?lI;W{KI!%I+yRg(|Cht!*{)BEo9q! zrhMV!yCZL%7MqR<1Q#eq@)&`FRK9vwCMK;F5f7Q#Y5Vkz9yeZ=45F2UT7+%&VFlfO%!zW?9BV{w(4H8Z;Oy> zzFJR$O>_=s=uOrn5-JOPPBu7<$?6fQOTsu47%1V;b4R@~w%Sqm>o8iKb+GaI&GsDdu*bb|^ z7@yl6TMaObPVl{I3rZ~cx#eVdu*}*kQsdOxVW@90{Z++$q<)G&tH6}uDJZz*7Hqa0 z=5)bag6C!3;~YKpt~#ylx)m!Xjgt32s@yUZY1*tF7KDR|y*ma-AXAWj-n@#SKYXNS zxp*cZG{yh8=o{pVF>AM{UC=Ltw&1ZZzVu4L^b#1^wQW7mH|_J=4z}m1Q2?Wdj8al^ zQAu^KFbD-e9^%k3Fh50w4L+HDK0B#fe{P;V<<>=coH?tuwURPU@<8?Arp{IU6Qftx zk)`E7MX#A+q=S-t2*J00`|cgnoygdwW6{e9#w61F$!=${&ZVSfp+_K1jpBvQzCL^G z+-Gz0CXSAQp+lBkF&rR)JEdA&oB|H(G=YWIivc^J7uj3*?8mr*Obi>4RJ5sZ_*Z8+ zj0}jY`N_pOvvdJ>tj!-WHht~uKVEm=hCk2hUD`h!8vsHn1ORlVH=d$s>~A`vsE_Ul z&ysni{3&+f;s9KIgL5g4%C*imChA=ZW~WR#hX=Jho&}}Bw~%GJSI{r~y^QrGv_#W` zKYdE|b8@^=9{s&9-Xj$C&K>1U3o^eFfzKvR17S*&71n(6HC+)*?&596P~oYA3XAc( zf=+N+`cEX`cD?*+(Tfmakb`)Az+?rh6$H8cheG3_27r}7mJNlA|;i=eCoItgB;*M zLFK3Y%d(q-f*HU?hmGxI#=XezXz+CI|K*=aMkbA&P1Ir_f+UP07I85Efh&R9^2Uuj zO-)UpF#ca}epergVD!^U27#OIhR`x{xW}t8n*@LWKn5A8LFb{7l27-09;ae$h(e}j z6l7(&436soMghBy1k3%aJomQ1{zv^A;ez>eRFW#(x?aOTLEs`*NjZBS1DM%9>Hbr3 z4l=O6AX5N?8B;Z`j-4t29g#%-%b|s&u=|nBgpCiCESG-|-2OTRjW!r%o#9{u`TK`2 z95>Qk_vz4TW+uFs*xYSMTWF~6#Kb|j*5z!C-)u&8yi2YAN3I- zG4b7|N8XH1gW9fTf1Aq6ikX||@OTVcROw;slM)_CqLI`nL%U|&eS`2ye2^Z=Ml<4% zSD9&)LNVyZu9+`Z($mvFk%?ww^^lcv-(Mq)6%l^){ga6WFwA_;DRTw*4+`;TN!@v^ z1&BD;$#`tPjAvaTa3b0P6^j&53)qhwPjbgHS}A}BJyCDEp6e}DC3sEF8dh_;iSKLgkAZT?9@zJPA&r6czz#07Tu?z-0Nv{ zUg)Vb+N?53?0aouc$=bstW`;a!&B74Rc`vOsAo{~jw_WDSQRMJ6mfZ3O4}3gr ziKp+cJ@T);$hUeV?xmErD6&H(CUU*s5$jUjlE5CUag3=(_IVK)$*~wO0omxids3*No^-Td z4#*xsJLAZb3p3U=ti)>Fp03Hzfw7gm9u+-h@Uj_OcPVy&SuH%;y(>C(?1qUXUeS1B z1NQ*SL!A=!bQR%yJA6b#==Sy6YhB z1@Y`z!Nl6qjyCk@`M9m!zFi3bDM3ddtEOgKMydVgu9wh>{$#BMoi71(DC^|&9+P{j zFVa-G*L#Qb?vWt7WEP)a+43uf7fvq^MY0gnH#NBk;xuo2-ykQyf9KAfdimO|%;|B~ zJ4dh`PxYlVfq0as>gtbN%sSDtl{0LU>Ti`@=~Q4ge{%dJ>4Ll(sU$<*^`cUaYg%;) zX?G7CAMbqsE&(F)jTJ!fqk*&}+5 zoeey7{eps`IC4##^!0ys80mM%3hFT^x{L$aX5MOriHd5*J}iz33)x#>820o_t+T&f z50b6lI8Lu09eS-I&N-TeDl$v777VnLkujN>nK6yFk%u}I&5(B+erwVDW|Dj3(>E4(f^L}_TmIwM#jwhU}1CEVGPLqcdk$LQC#UR^{b z^%psL)*9eG0&MKHZL)g(#PR}3KU~TwMZoKb zs}GgD1YHx!hKFnvNDjtBS&V(47QcYs(b19VR|(Q5EG${E{0WEPwpdwufqbhilGcxVC{9PVF3RYkO8Q+k*qPb3LBe<8{N*BsCy+(w;ke zdlZ?mPC_E;DUh6)O#sOv=^LN_@vHfwh6$5XYd@M#<&v}9F=%O!*Pb&o&CQS#C5?qZ@y5j4}o)@Q;@1D3We^6fk4oc(B{+G0g z`_FvF;#`K$W?y#<%=;r+JK)!xLPDMXI|2MR(%*QaodCR^0~;Hg z5ad%BIK&YlA=j~`3Z~66SVPvRv#EjRUU2Q+7ENgYGXcm9v#G z7`$P|?sa0@njU}X9pXPD{}olPqmsIuZ7j?q$)>$a6=)iODTiO>uY~sc5E^YR zF0K119Xnwub0Fp$VV-!r4Vlb+P7$=b8~ZqH*ZG3adA;et5>ZSm62!B zCu+Yw77Njfka&`z=H2Ot&4fyNUim8*|LuRowuum{9r%cjo?aT@9UYBVadGL;njyIZ zXuzm~7wHvhMcmGgFhaQXcgnl#)C`1Cvc4hr(XqOP_^9$9U^cq(V?e;op7zi0BUXZb znLYegL>2MCXw+~PqfwszGXnt;`+2viU)OA7#CTXb?XO27#uvhq$0xYv0 zHL6jC>h!Ni3&MetqpaiR7!x>pVS^d6vVMdui{~f%$_*DP;GBkU9W1Z-jq+NsL7>|_ z)O*YE))7>}{~u%D9Zz-t|7{dyR+5z{A$yaZknBpxR`!+|LI@Q_HU}qr?>!IM}Aw{Sy&6^X%(<$<=S_d^1fGpP#MkOMT7try zb_E@f&pf4(E#v0$y}sF|#*VE269@G7$nTl|zR;xS#;!a8RC+JtB_RB_^>GoXPM*ri zBaD1o>TRBEa&CF{iOQ!Bx*`%>k9NEdcI_m!qZ?~{jxeR>aNX0k-{t`WWWGLCKEoVN zoFNl{eUkks3T3j|EG_Kt8G}5yCv1n67uH*m4@u@2H0$ZyL5TDY1iaW?^#BG7zMa-9 z4dr2h+z5T=imN;^$Sm0dt*1qXqf!-YtYaBA>igIgO7qorI*4LNFhjY0=>-clEe_vj zP_v)mmp9=eHofJ5h5Xds5_x-A)@=jHWaM4Z>-3CgWy~S`YalUb{5d(bkgDeQYd&3F z;qgS)3@sOmCvI^X)au7@?hn~Y?-|pLS2;jA^(ysabWe`4TRg*zb#7}3Pg1@DAf)u+ z&qzzfd`b@OE;SD4^beW~xrPmSa$f$X6^B5!aC1-r`YfJBx(f=-cyGxQDT7z{w5f$qt zBjR#wN)(%(ZehiG!^u%mf>&-?A#UzsGQ@N}EQA{i9Vze%tnrQIF=FG$N8T~(juOV@ z7OFZSx5BJX$6#nzP$H#cSf5c%9MV==a8yd$C_9Dl#<|Cz%&s+^v4+rxinNU|I| zpscV<52;85@saFtp8gu?Sw_^Fgr4zRH(j}-H{3Y zNg-Nx?)_K^K>C?|vml>xM@^^BLssWg$PAK<{UsNCMi+)AOeYKldHEd&c*{Hh2{GMy zF?iKLst79zu>0QatEAJ2T35X+hxSz+feerj?%FSEDyvH(y}nouw4gS(tFugNcXUQ| zylmj_yJ!wN)4++YjXw(_rcYl?W7JrZ^ptZB+lOo6lVUGE>Q zY%d6VUn55*2E6Q-!f~BI&g$hJzXoWwYS>+`uUS*EUa3CG&t(afx+0jTTBPEc+^cG3 z0kpTbVM(BC;*K`A77J`IakS|*G`ekgbi+NbIi|$qL8r$e<<5=1vu|IEhI>9hCWMt$ zPx>{tNiy?tnH?T=vYvPrJ~cMvuuH>`Zv)%K3XRu>H)U&aNv5SddYZnyHrZ2?+Y(H! zoYfbGJ$H5cxji`gRas7{4oNlMo|tfRIIZ|(?F*5hO2Ufr;kGdzWCeK}KYW%!;?ScBOllk4U56ZsNg(|0ym(s-mWimr zhIo_Kzs91kSoF=u8r&kKGc@79@1h3i*S(}QQ&>%(EVXIZhwCs{MY-3ERGIzBAR&6~ z#;U19g7yL8#BDc-Y}sN2Nvi|+4k`QXA0fT^YlFXL!=(2m?!wk8A@daoLy?Wgz_N0s zdB0{s+|i4bRkds=ekg#%Jas3_=%jhGnUG-|!qW5oaBJ2K6bN4UqH4E>lzC!ankd}e za$#l#RG@We+*0Wkn;oWKKIRP@3Bp!$&Z-Q$I=6wO@FvZ0=mmJxz{Z&2$m=2E*$lb& zKX${_ft2&G{c<7ZNin4)Xb2$s^cz?!ZKtgpDULQ|=;E?6t%cjIEj}4S4q&7fglF%m z#QDFEQ=HRS&1393C^7;=S|f0vwjB9rk60NC)9Gud2a%vRkn{QdN#aHiKG9lK?#N)9 zaKpr5y@=-hEmX{R^-sbFJF(ka3oLqmL5^kKbDfWC3I|EmW@c&@+aEHoJa1^QyqH-@ zF0p$3VC+yw3Z7Orj6WN#ZQdCYX(YV3G?|qpXy@7B?Wxgi)%)#NpPM;{L$C5nA06qX z_uoq$zu$5WXlyY46j_ooSV=hP{!&TT3Y}*t$?&q9Xl)W^6humgrjY1lf0KgP9FVn} zU)SPN70m)D+4vXdV+7wB^I#dhd*fK?1>l9$f!B-b|JDKkr5g{-GzNYZ&i?8fd$bu3 z?Uz@pzi@XmErmOoV@KR7PfC>(t#Y_X6PGr-dm>+VYtGBznDixzn2s!2^d3ZW8z?2y z@|`x|;_ZZ7F^q$Ztt({|iJMy1o+||fiJZ}ub{o-v!=mgTFkEKowm!jpbU^-6DcI`L z{f5=HB+*n8+E!{&r);*2j7el8n^>j}SCUvoRU_uoba|r#>dUCOL%*bbji58***)XC zA?Hq0N*Fg#IkdFQ_IU6RcqXv)sRC~!aYv1i7#5rq@>mKAqtC_u=Owi{0V7$TMtoOC z09i5{poR)I^4ZtaMS4JK5QHtdyPb8GICJ1~E9b|vhUDWEaG3J|y3Duv%zAQ|G;eo{d+jGynsi#5+Y|A;Lj`EK3@u(+;WqVj^s7PN-y^t(MtdbfKkgb~RuqcxA zk8cTX&D1_X&zBX}Z#vQJk38~aQU_)3It57e);>CV+H9L)Jy7pqfz3dnBW;RR^ zlL6FnOK9KXP6$to5=MbR!~`5V`e-IH0bgEi2MHL_0_wbfZFa&y5mdNh~_^x#dy8y7bu z&Sp#9)%#s5fwE?%r#~}0NCWW5-lrb`ueB!4Sp!EF!X3sSJ*fYE&*Fh7d5mPNt#F09 z-!ApQ8<{6Vd3{j|3~OAR1GsO(Cu>)IUA$^d|0g^ zlR%6n4in#Zv-clA{u0#^At1TVzM-dcwf6PlU>!{7VUT%RN zTrA4x$lzLIRVSK0MTZuMz`BzCo=wbRU#KIJH-r*yUQ@bWVg4Y%hM(tUCdCvgUi{4h znX$fwSt&^=zixfcgS>vFJT1H4;$4YBBr8&W!>A$5l}74y_()xCp*zvp8EE3@@8jL* z%kSErRkHY%rk_(|HEeJAtVf3VE!H8)91_P1S@;&cao>G#q=wnoGys0kyV?Wv#*-ul3e$AlMc$0zydl7I**6}<%Tf=d z*~5e#q?CgNPzMpNYcsX5yeEgxw9aQYapWI8_<1zdtm+e@Rj=%EH!uVLF31WRqJ$^3 zmL3O5ft&NO%1NA&Bo3yeb0D10`8;b1#EKURKqpppSi2SFx<@xz>k1i7l$=I-B^@tR z*?Kqx;Q+?-XC3@?3Nm97x}_??SeVU=Gi5IQ%H&U@$8rmY|L8vey+Ba=X>lf8%)|+* zOZ>dZUp95<_-j%sBeguXeT%}-<%YQz`PioVX;6}#eFtg)w&rOFUhB4E^~vM7rsPRl zd!V`G&71PcUOo{X#)ag4M_lU*y-1w)3Q`Ef>DhFhJ1_yL>AG$oHk$A8Spu_4?VIJ& zzIsJn$IN(64oJd#kkUzYTWzlC55PDkdEnEP{d^n4>yOk>QZmjEC#g3|*GLR@fjbi|w`4*_$#ZEcSu9<(T;@iz3 z16DUsz52+1@m$LV8BJ_@+C@1b@>JSjVBj4dv|m_Wpn72!GX< zn%F2#i$Y2d=@(-ee?6n*<=Cje6z^%*F!n_g2br^lcPj_1)$|2NItQKxoe0c zdmt`YDupyI3+5D#Y%xmoi7L@4$oIjExz=%ST`obC8@fya%L(tmw1}; zFKxB{1~`aINK9?Bvf#Hk_b0vwYx!5nfzuTEEJUzT4ZGvj+c2qv7?OAB^VMV5&Sj)z z*Eg;$TTz2oKLh$YI#u=oO9AtE=ZWrieBy z)6zV8n9tk|o>I3&rbl}7DVlgm&bG5>?D&1Vd=|uK5)+#!7ay<|2LjereV`)s&CI>r zp{c&6izRig=DJ6=tajGO@W4&{xdfZSUA4~^?drw1G|J$w0&JGM!fzhIu^>7yYlo3% zPFB_^OjI@?7N6OD81ikp1S}uJdiRtySRZC6Q6`D5e`y9813Tz?{a7v2UPTfj3Fp!} zLP5a-YLRJ`*}PVT;*=+%0>rk;Zg6!2UqbE@^_zC<4qW6Cj!;Yd)nIWq0jTzYe6Ocn zkigks%n45>*$8Wb({xP_6>7+8AcGSE!O*i z+}p|67lu1!sA(+jFiMtCO&&)!KznPUftf?A@CuiX=EicZ)CRiI7hHapjf9xD26e}$ zAlHrRS^46K(yX8*8kp=u&4Ld$*nrjU)n?Gmm13^=6fVYiJlkmHgP zI5;~0_&4ixZFkHZmbrSGRYaBG5d+R$P8643dnmcDQ+aPyskF;Wk@m4IX>f;a8BeJ= z9T_mRfJT24`J#V8>otxrm$|pcgXl0=#+_ z=CQ|EB!HL3SdZpD^NVLHbsDt>lwD2MS1HcFXILFzdB=w1nMJJ|Km&H2d;~ zVe|Dab&u0*;788uqKln~9_=`xb_N*_xa@$7_4>Y#>CZj^810n^^n|v{c8+7EhZazz5d1suwKnYGt{aqPEFB zY+9I9G}4=(HfzsdV<;n_yngms)BV3Ik4x^2Uc6;j9*0RvHC=T2wk7Vq&gfpL;tK_Y z)rSya`d;p}k}nO{R^;01G18gB@7XKBa@PqQin?OT411H!S2uRhmAjF9>vcQkh@2(AlzCV=NnFFL zGE0}k7INtrDQ{?T)}Ht1h8$fR6_;mWzmMzsv84kmai>L=jjX`yb=b*`yw^H!1eAdJ zhw+&WX}{~gYyIZ|Fg_gM3Gz18WI#LTCg>|568mt18mkES%14ITSE#VN5ph3DcbMD0 zK^9!6Yj;>oz8c)>N`925MOu@!V{lrtT)PR?_9pl4Jj#eJ_-;_H(AI#dbmC{PKd6u! z1Xo+!kc!)W=1QRrV*tVK%gkCj+pu!H3)*8fQ{!12pk`fniYZ-1RT(M8vN2cKjF#Is z8Ve{02uYRkb5z=ovwZ%1nZ-m#T3xHh51%kUcoW(Q3gPl)DnMQBI#``*34(T}-A^lO z>pr5u^*#K=#$amvmiY`ydOe@ofw3#uBGzoER7uyVt6>TMrXk# zYShe+X_@Qwm|N|0dUdU0r~Gi-i!Mv915K1@GIDo;cy**hRh+oHmU3@^u@N<@yyqNL>**cBegodyZ%k5aOGGY>*-if2yA z8t?|?s5>lxFE03)6RN0Vd-$B*ccj)zDeBJpaFHxSYE}J~lymxVgm3G+W*<5qibpE=PO?Vd)rYq}Gpk$O)Ed8g_%?IN%StgT zJ9`#INc(nZ0X?B1cc8N*c?q<}3NXW&qO^|RtajjcsA;Xwe|&Ky9MnJ7754{AfGm~M z=Fo)H&o6AxjndlXH$nNDqUk5{rUg4_y?m;#l-$X%%Ipa6o_4RShg;M7^s{+e!T_Q_ z-YUAa_Gnj+pLD{u+@GL8w}LPBG}>Er=LnEc1RTfxxg}!YpM0aiUjcA8Axzv}NP* zC7J{FnTk~sV#f03coPO0HM(C$n5$KAYhq4MS)m5-8Qhf-_6>z4Lz&zwin=8V_5K}-e zbL~fsr#8PoSoiG&4K`5`)a$g?San#u2q@eyz`c4zt9G;EOwY@I%{S-aJTr;{I18U^ zr=37Ma)7V(fBrpnMRzC}B<{a|dY>Eu8cZ=2HZXy4N~lkz+*r|x)Nt9;{8xVZZs@k5 z)Hc%8BY{%g@?ZtnaX=DDNbf?-X+4NudSctX)0S-ZkH2}j6WGM(SrkQkU#7lfyn`YI zS7(FFX`@Ze$7``KX>+(;wbWrl%DJ>N`yrdTQ1}?j5GW7aO%WNG0SEveZUG?|{cy+9K)?BZ$jJOZj$UOj5f_)IB=olTWa zgateR`R`8O*@|$W|Je!u-Q2%^{g(Um=o9<;6j88lRDWCDpIsdW!@mc5v(mVL*O>o| z4~=Yq0ODByP7o#&G+HU2{|P2>k5S}zgWu|hL4LaaCErFD^u*)br-dq@EIIt!@#0VT z1IBR3@3_u8Bw71D`_ue8xP?Alw}Kt?bw&Q)eO-`E0va@B z0ZAx#(v29vqXNk_(&g08#P*Nyz&Z2N)qb~s*$>!&h)EuXKg|IoY7z`^G$FYSpxtLB zGvMFmUYuU*j|_+L)9#-~KfK6x;!cN4a>5;Yc1!N`6Ka01DVvJ&IiH!Jo&+ z7`}398S)qOXQgmWj=Pil=4g%vVUA$|EiMTUa=|VqBF0?a^TPwBM~~jPi=17z_&pJG zw0`B~X{7+zHMX^Sk1e{xH_lfkuv?2_gJ@`>(TiECQ35PMgIxs5%M9fw`y|~l?kWlC z?z;IOSJo*Vw)mv!`FM>D^eX2i&PB?Pok9rLRH{_w^i#197M2HqO@8|~tC5P&wI6qU zr{Cuy2lcH$V#@9R$M=5eFT<=!-wsT@pYg~P>62LiEPIwLFOrf*1I!3$``7(t5n}M$ zBIJ)x$XNB4MTpY1kf@oNFH=rdCulK1bWb87k(4=wdybZ2vqj?p3R=iamdfBSLHMfS zp`k=O`mnGtk&d}}Y2S@0Rxb?wZDJ-K-}n0BXGY`z5Z!-%bv5K?F`kqC=VFN?E1^cf zS$x6I`}ONr08<&S0^Zhol^en`3HKkLXO`f+U6%6)wSvo{?KcU}DUN&0&1n>RRY zFAe3A`x_tK6US%b=YJyT0aT||?DHhG2-?XwC}gSpspA>>{)m$X-?cgu*jQq22(Y|{ zM&-*Ie-&)r1Hjbz^Ou`Cmz68%JXfyUEz{FnwoW;3>U!be&q$Qc%eZSEC0_lwfX{^b zQmiR+hn-(g0lA`*_!OVIq@?7^jT`pJWRlR?*T6ue#2MkEp{A5&&B!Du`0V~~1CdVw z9rk}XOmX}ouMB)?OKGuknrV8@>IiU=eQf#hRQq{HxXkItEd{BPd~O*xct19rw0yt* zrwb6!#@Q~vEA9pa-#{hDM>A}qM)sobg?|Bb=5GN*rn`56?*i-Qyd|T_#<8r^&wB~i z%WoMfe7f$+FQ7Ij(R*e=`gdu1<`rIo6@ZN!v*QI`yTDIFC9}`z_+1zvvcb~ixJMyR z;UGeCc7@@uqsVdLROtaYP&Lze8Yk9l_`Cn{egJ8m^32j;8rVm-0J$}~xR_Vz;Y;m5 z9%_4Y4DEy%JiB5Itt>caesNWkzsH94_EBz z`3JNzPS04dpO^eTb2MJ&{`1%3o|F40TYV<^&yX-2KLMWqV)zAjfPg^!iCXo)2&(s4 z{}!En@+1NP@E|0iO-=WJG{CaBKlz{VtYBuifo01n2jOqF`kep9TjhUwDqQ8iv?TcX zf!xE?cbFSDrgd9PiD7PZvJR%PuQjBL7OCHdUXBGCT5N=t1IJ4b6Q0!H+#&UU1s+ zxh~)yaP`8)D7GZ)`Gv5iia%*^@u*8?h__(gLwA_NKTxg5`xeM)jU`E>P@89iv|^0aGqr`^UOcx?5SVP0kbN zwaO_5oKW^hL$x=^;S;~OGy2)??Br5KW)~Z8aQ~qJV}kd;v0B27L5nEt-Ve=ie9I{R1Q5+ww76?#t~-|iIO3H1?$1`r(pIe0 z6b+xny7eKfjG#eqZ{C4sa@Y3>is(i0nMX>v9$-E{Kb6)bF~B?3y7;xdiigQ7w25^v zaG5^NGQswr@<^O3T_vQXkk=y-Cx`W}VdbwjiMRO5=Z0Fzgwjhj`MVn|jr^sCVU*tC z`N~ZYf>O!SBX0JM$7ISbBHSLnk(#xjWs?6q8n%XNy#jY-!6~X zitf($%;X0B4wEMlG#r&5#+G0GiWJ(7 zt1=9~lK~g9NebDbjVTL+#vawnY9-t>`=S0J3ahEs>0;lYW>8c?DIDjl#ZJe@WfNOM z!-|SygzrjY5=f#b(ucaxgFKQbca%eg4l9)~8x;NOV2G^qttgs4b8R_#hbXviW#DEZ zttOY*hmRYjNu?`r(IRUKo{I}u{b4M*GThb1E)3Fv19h53}0Aa4orBkW|tbN^etj`~{&V7Dtb@{`1w{0K@cq!Ct;`O1@&Lc&_- z;5Hjg{0^@qw$UOkKhCIX?X#soqPm%L5glap!=nwjHw!IGOvE?-%e#^cYB)?hi@W&Z zb(AX_8L4Pq#LXyct?bADHKQJl``)@$`|9GR8k%L^AsTAEV?Aqh zuNzuOs4_QXgMEkkwTrEAW>YW0f@_nA4VN}@2>31-pWBH$%du#W^#Z{DD1+z>NFI&1 zUgYP!KNVe(Is9uVV4O;FLOaHA(d;CBlZsea8HTtv$@H-$;yA}S{(X`(@ayEtjP zSwo0d=51E*BW~7nVrD$A9E0*vwR-FI)d{^iJHaYCwahJrv{A2SK4{n)-`7^rtJFL` z81g5X+_T7x|DqIiu~6c0jL5?a>zPAI#iZz+uqRTI)an^JFP&xntZ;GYyZH@|1F!Mz zjvMI7l3FhPLb~}8zebyWDN*MpQ?GjM$(2P5MFT`AA z?WhGgPCL&=j!J?#{Wd_?Mh<21;l8xyF_Pveao+`l$9=*;RPtwfRb@7Made)p-6-hJkw04**4wL#@+q1%W$8+ag-m+@9>e(1u z0`Dd~Lvbs@xIP8DgvTlLgQ7xk!-0h&zh7w{%e7l+D37Gn37w*GV%pn|ft5Y+{8`rd2#}4jv;so8$h9)Br z>?{XgT3^b~C-+z@RuBzt=3L|_xeoCmck#BPI5)u9dw&?cAy!#e5gK_Mt5vnZm&M?j z{7Dv8v7t~kig`9na-kUzkR?Fk{=sntrPO zK;=3(I$(=qDhbM=Nd%+i=*3@8hf9i@!!xAUihD-v{r4qe*gUc|j(sS8zVcllaPXFl zY)GSgA$TVXBRlCx(K^7%fMDqzza4;jwrsv8$!=9@cP)Q;HI6*63kDu_5^_$!YI1v?)M(>+5-`P3Oex+#g5J z3`noOdjnWuCMI?SfRIIv8~P}hiKX!KV`vd2DWi&axn{PiQ$?>d$tdx{Mc1`f7gh25 z3|<&Q@yVUuX9rJf^cKbPT#ucG6;V|tLPhxc!Qq2h!xiY^rGVOz@?mU?5x&XS26)DO zbf|i(1!dE~%N6cKA3*B&bp_NWStz|6 z^iJ7&ytR0E#iw9%v+7ST4Hk>U`3LL!H~la<9i){Am~cD}L%IdtlnwuNv1G&MNx1+i z5lCw9L{4Fgo#gA{vI!JlA>BdQL#?>)AGv*)QpEn;JaDE20 zI{b|+KAkQMf(?<_?Zddi@_L7{RR&K-3HV(62tv3$ErSf)ebSsZ!!JtIEJ#rV16lKp znhrHx))py!GoB1Yjb?g>Xz+)Ye!=l8k_x?5D}~}5GWHF{xkl zhTpuyT3=$Taqtul@2e7!U)s?OkSRh9ej{c+dfb-v#_*}eFvnlx0cYs5YwSl)T+jb zL!27?P_SUju}!0Ftx&A@5v$RoCi;HKltT6I>OSewaNYse!ZKa)mlelB(s{B7)X!D@ z!IHh5t`oj0ksPaFED>ii}%&Hun;XO9=5)&y@YkLTsHVG z+Xs_2Mg@`IS<>VTq)8cR`Twi$((x0Z#z!X=dgm^MD?;>3j+294D zN|D}23hMONmpjd-_r%+I1Y|OC&6jTZ*7do_@$k|QO+=`$*26mMP?}IA0`%90)qb5{ zIJP2CEUq!ZitoD8VdC#5g7b&9Xi=+<#T!=HOb>vaq3?}{apKImG})-Qn7l@-@=mtC zN2*O}i#=YmAkRm!fkIltN_kg=f#q?@RvxO;jU(ed)_eQJAu@Hhv z7it!}s1Yv=EP9aK1m4=clb3arVToEHrtTvQ~^G&kQUBZ+?+|2!sozq>^tT$8lFE=CP(8 zExD|nJ(=A1eJ1v-V4Z21_HW5l1>mMHuaa{lpm~p3*A?PR4yGRo7M4vwNoPo(0F19oIaXA++t*HcWVp8bRjd+fhA+ zYvug&y>H#;{a;fZd1l+O(&q6wX*&Yj)JNi@;r3iFrtNPU2|fBE`-$|m_xrD58Zl`M^!Mc@tY@XmqW&?!NY)hBK+ceVwL(n^UXX7DARZTHSxUZfVd)E!ufGc1OzsXs>^tOW08+k3&0};`bgQ zX2R$;yTu>gV0EHv>g0a)xkIB=^vrozc-l1Dbb*{a_D5TYz}u&HyhTPL%|({eKeUfWqsq}Io`t^sRc3t`mZ|M zrL`-Zoj4DQuLXOhiG@ZG(3hhr4pS1w7zQ%wTcL5Z6yriC#dKONqk?)`S3cZ|y za2{lTdI`TakncC+?TGjx_DdFcm6*57X>2@ypL4UM?;Q73&2(e)Ow@r2(98s(uUlZ@ zPOZ80sIUf0V4dMfp7fVX?D);F0^f;-4x0&<&NXw(f=6&D<@X@+Ehx{3_t2~mBO3eefUsH)p6;`&Go?*^SdcoxIutu_<+&*_rH*@_oA zh)O>$ch-fhbnduoO;Qs2UNyyOksK^+5rXrnms^aw#Iey|Lq7x}JRQDTh#yAD5HV(M zx^|e6+pv9SQKDG80jsMa9<}v1o4MEVb=G>B$^$R@pR>G4KaV74dh;3TEMAu=#c(X* zT&+Xr9yilA%JS}VuHBw@SfWGFy`cr`kU1Bq%On$kb2?kktR{=Znh~kxWSF~HhI8_8 zW#JGshtyk^a-Fd_nxs|l1&0G8%g=ya1vvA(X+>9=@tH!}M533$yp zQS7I|IzpsvRH z!y*2fVnq*+F9&Ql!!kB7T^OzIw_XC|lh>RGubtV0aeRATpWp%DG5&VWzA#_p7q}~% z*?moeN(TA?@#|NB{JW!r9fmSFDG~Wl*SI)zSY_qpCxN7qu=qY}J5=1QcF#oPE%)|D z$r%?#&e-H@&bRTYSE|-4X&5~1K!_972j?K*8NOzD!x)(+?29uyNeNRfI)PX9NQ<{G^>y+-jU-X4!*hn<0^2@hVi2|^K-|GcH6WXmsg+6@DPmB>u zP@at~l(f2)sfKyahjzWK->#PZW3e`DYYe&nXLkSC z1gauly}ab$z(&(@vBB-f0Yv)C<_YNQ8HJ3AqVEbeWOio2**hg;M>mW5+{EgD8*ZlM zoBJUX7Ndf^nYg9-{z5uy9cL?MetO%4JIn`-in<-mu31V${rvfJp!ebLYtin>lb1T0 zV>b6WPd)nG>{oufMqn9}6asd< z3RGBp>f;%O;%8;o_!|$qbSfDu)?>kTQCP5Yq9;?B%&QU-^b4g}Ek8(+y*a&isLy66 z)>9`K7QIlR=8|{6F?VMilP>M&tD7-v*1HxTa0?peKf~jFoK`N3(X|P)WKjYGyKjb9 z9Wu6+Wbz@yMl$p(5T#t4vR?G}pndiVvpy^^qByuS4NYK92%LkN(~F^J$LZ{he+W=a zIr))d41;~kl?)H8lah`$Wi;+hPANAKxfixFbk7*ui{ZR+sVQynl5B~PtzajS)kzaU15 zJY{glCllp;GW}$la^g+O@vQATImdjN8c&0bhqT17cReg-&`$n5!>Jp2A@g~GkTuV> ztFYbtbCZ09vK&?eCf!3^Zqwc@OD{_<`c%86gEyl>FC%)v#{@laCtXfpUamZSy zJ7R&{cu8ZMi$HC64CVG*Q@rq`XmYo+m|VN-iN=?Sih963tDbn=;EgamNNjj z4}3B%DEcCMQ`DW_`eE?(?wo!6C!m1Er{0JV#0ctdqYptPObIm@vUamPvpqdn?;V+{ zCdhS~9XO*eX1=N-C`cBd|GCqc zG0Eq+qI18CWzsydrr$*6(*0ni;f12{)C~mzsdB5`&jfUWTk-*yjN(ui;evw;3=evJ zQ9I6iKQ${d)~r~^y-J)~)M{Ku?w?je>QD*+{o&vEgYoPV&}lT%6Wo^i$dQ3}vG@n_ z5t)a#8G5HIV^_lbiHt+zEcs=mjBLUV_9|S{)$S`ENC|c3z!19l3VK!7N_J03h4$uHT|^VJ*!XeC`bg#kvGWE=%qqn$*fO}hU!)D^P=~DHMLsYF7bgBBtLG^%HcClbRukCF`r1Up^oHT`l z?xB@>_U2JToHYHME1O;63rMVH=2le*4Ve;zY%)!~>tHg?!Ix(i?z#u{#1tV|mm^tD zIJdMM%3MjWs9b9gT(EL0@R|u)x(JjDPO7qsL};iu8pw3%{gp5)_U*F{R{VnH`}owN z2Eo(KhXYOhW#-u_iXlGEA**?A$wtUqeS4ZE>Wdc7;#)$;SUl{8#3tj{P)`GHy1V9c zyQ)r;BAkeftNkaN4`z1V%>(MjqSP#|=W>-Ub*v3pY~ONt#$DAw@Ii2k>FH$$ZP-`n z{7eyF#AGeOzG#&p6hJHa_E|mFdR!~1E9=a?*KCoKzC79N%uFHsRFi0{p2}`#3N172 zwAHhnb2PTrg-_WbhL|=O@D*4OGXGgk!d#`6B*Y;bY)3RIcu&B1^$L%&tZ#?QUX>q9 z@xb_@q2+Fn^qbFg(xkMO!<->*JN(y}5WN`(9b8*d%8Lp9tFrJNt6`N$ZB@4qWRdga z6HB!rkeDjN=I0eI25tbC;U<=UmM;EXTHvwH#LcY+i2ZkdBq8W(XZyWNe}zp~%9ajw zJ$9r1GIc|inBFG1-rQ{Ir%Fa|xW-9LrtE=Wy>mo^KD{S;$UInV7SI80UdY9qc z_}fm<)Q?@2M%t~GQ`Fr@U#aP??OdlVk1Hjc*ZfMdPho+_o$?{yqRLk$f6|`^gycux>c#$z8lmtK&tWwPvy*>Ke}czn13xeKBs#dGDJ>(hNORWI@wz3; zhvUAbOM1F9Qcp=~-Cv)qY>1y!LMeT@s7|o{$;T~;4q0q343V#e1Fnh5#rur89a@!I ztK8$OR9UaTZ;(d7TYj&GEGGu)*1-{C+LtSBy3ACtStfpPX6xtC5+1N$2RDB1sge;* zMfL?6>Ok&ep1Xc-P0K$Y)6+&(?BM&yt}+Z8g}g>&Q&UX6^*DFwjwvqbyQcMmj` z6+eEfKO(c|ZI&;9y}XMXe&5II`i(P0I%cS;?iwd2uMcNI%k3t0PsK~yLa12ZybSea zS)GI}#EYiN5?c*FFW?qw(Up(??(VV#QC~b_Cl58Uw-H<#P!+gg*sO&zFx8%*(0%);;N#m zs*oS40C%X>v3Al}DvfKTIQ%bWBc7V(}&)i@o%1z+Gi9n{~W@`<^ zwleVKs3PCsfqf0*! zD@t-k_H~MYIYn;GX#k6NA?sw8@86lzEeCngQ~NLQsQ1fFMb3zL{+=W-Aen%T-`a>k zLiOn#BR`x6xD!ry{^cW`Sxw(cOn!S`q@||Iy}Anw7{p5H{^iN8erW;{?aV9-uYl%N zsmDi_H;>;`UMKqd8O}f!Y~K7L#LL}S9eG&mk$2IswbK=0sEEVF@M-nWI2JH%oR7-1 z3@#FLu&6==Bl--)ls@Ya`|$dG#NQQM*X+$#+mJ3s>DeRDrAG(wO?>wEqkQyUHX?2!i@nq~VnV3k%cy2J127B%uLYwGzN>X1#^{UuGZR6Y*%B z9|G+(AV~Ng6c*}$2GhsulV_eye*yaLr4XIkJOmO^g7EgqhihC)mpAT@F3g-+7XRye z!a0DlB!I^h(@=~3prE0yd11ozFAsHx6PNT^D4mEJP`SMgI4xjXKX`8Ctp6_`>8$u? z`5pX1jEU^O{P#P8|0vo%D9;uFhwKBm-=DD{r}HFsD*X0ic9!i&5*=^lA2&5P44If_ ztc~9W2cHVTbT!@OUrjs{&i|hkTS@`uzmkR;m~yxY1ci62g}DC9T!ITi?6<)Quzgkp z0wq_ubfZW@?`tof`g6P1p zlzW%(tiI`gcAKC6MPP6Ogo$r4F|F;6G<5<2qdx&jz;oVvei}GGUv{$nmzUI8_U78J z8`3`YTt5nEs6m0<K-{+kl|)BvtMr zn_>N38tUMTuD*Z!5k|b@TS(oJx`zN9+0}d3n0(!L)N1-3M6J%ZR^_B-AG|&X!&p`p zmI?2@|I3VY$7&%J2ZvlkQBLORKj&yp>zCyW zHWo0qS?|)w?2hAecom}1Ao%sZ2k~nPIuZK_nSkI|%GjM1CLx=rn1%R%p1FQZJeLk3 zNZ<1vfeftCSAKviw|nS&;_j1DQ^gbO%zy5&nUlxh(ONq?fND!9{rL|!)A2-1ruM4R z)ltXDAy*si1nb4;$a>!wKxk+vS5@3$ux=wXHdB?>YfHiDMOQ=yza5suONqM~L6!a?8j3y#5StXJV6lLFMVANMy#z8=2j;C90B*SRS%Qp@n}8 z=`PDTf8XK7?aW}wqiP_hEt&-V0W$b#{q+B z+lLLHtXXg4vDV zDg!K3ApG13QJh_UafG+Kt)c!6J&`pMp%i^(YT=*qN{%DMa|;ewst1SaK~kI57QZHz z9`8EXIrzoz0cX6qUmV|)iuihpdg>o$0_ zO#^ty$gJLQX5|=AxADPo+A`}{i_{FQz_nXByQ;RS>o3o(~}BMI|s;&T+cNNC8wcpGcW$fiZeg=;L~+BR9_-T z2o+CcgVzbmo6GfEHT$KryIpK=ULKK(1d`mZKH8~p&5HrLA14&5?*}WrF!tzgXnyB_ zm~Z>}K1)=YbPm;@a2{ygYc zcVxd#c8a8oQ*Sg182(W8ld)?Cy*iPxqwUBlK=%#v|K}RNPqY+XB<&Wl*X)NLI)~dm=|!+#W2Cipu#H&MP#tlH)xv~GTWu&7&s zLA7|rB`#aE3>NLZ5Q4ZxvAVa}qsWgPlTVpG8-@kuS^5Xpev0~Ov&kk8q;)6AJ zfeh!^9!Vna?CciNH6V31raWxURX?AAThRQGh_XAx+$@wAM z#4ENhX)ZmoJMOzl&%>&o+Rk#cz?_9ZjKc|Tz^V^+_IvQUh*2MVm!dfxz4eEG?(!0L z-qrZ;nfB8~rB=f%R;WgL+|U`qn|o%g)lzjHzBp-*BLz%n+g07R#9Y;)F)EHNl;=l& zy)3H5ipBH0CKwbMeK7YHfsa*E?+^8CHU-Ew4Cf#Eyr$1NK1q>_)P$ zAt9SyW&X5Q$fa5y8LT||U_PY5g!do!N>j;gaFl})Ju5ZM@%WbQ9z^n!bLOt7O4}X+h6kfVSV!VXft6ewMK-~Iv2e>yq|3?Do=W^;T z2QJYH?;pAvg@5042&|E-Cvf6LIwiUHxYR%pV&tHrPXVVmIa#=NSQ;6N`GH3bJ2?&> z$(bCO+-bYq4-9P$XW%Px#cpj`6w+5oT~r{#vfOotN)CGTMwp0f%W1eZgnEB=GRVxhf?nf+QqNI(ezU1vgaGd7-O~Ufiq4LPbE`nfW^V&nym* z)ZMJGMr>6MrsVdmIet0>htSa6wt#?l&Jd|k(_pP8an1ISa=K@QHL_7m28RisP~g8u zT}Sb~pAOj7JLQhMJ!!`ljg0aMJP)lR9_6Q1YrPXAnYo@m=E^cTzC#T&1&N=V4p9j8YG}M5%T%T`Pr)J)t43uT};=yn(W!&1cI22E2Bsm{BUp zKMKgzNWAxOYZjJe<7v^GAWyy#aG+ygZs#Enm7`G$i1|oi?mGBt_u8 z!4q$fh0#gpx!l_9K13y?GyQq|cM)4X5b0urMBWJ?qA;H*%I!Va-JQBmOYXd%p|?_j&YL1Asc+c* zsix|}1Scp9HM0I;PfepMNxiu9v}*{dL_x zI2G=iFQ(UQ7F#{hqQ6PzEw+{~mQVn%dSQQD02=t3CwrzThNU!B4ThLWz$v7F@<`-S zD@B3jBE9Lv7?nsbuq`&{Z!!3TRI5#Q2bXtjbW20~`(qPVl`|_$7|rXiOCJ_-+l)R>V-_C zTIA0Xlhq=W`!v!NOZa#iaHv%{%mnz$Q*0$l@$#AX5vqY7Jx}JAz%y>Sc?NaEc;Y<;jgdhytn3ZUI)8AVIOP!b9>rhQ$56uCpizj! zzJZoHf~kucWoo>O@-o`Y?Q!;qle3=do5LFMr(C5DjO>tlH@N=`<1dY zmZE~h|EOS3zruoUghFJtYPGO%6=&~Y-oL|;xsCI4(;gZ7cj9>3yxiS!Lb@}9sPDh0WR?CM4R>#OAkrh5LO@Svc zdS;IdU)`osl)afH8wQ{;i!1$5rw4}lsQRH`gbxFh88tO?^BF65vCG*tFz6}pmIQ6= z?19mbl=f%wE&-91F~PNY^JG^7Xnz+hN#-Dq>`}4gd`$ZiuruF2GW6ylU-KVD#_1{2 zfS4FsvKL{yL1gXP>!a2PjOSt;{)YdJuL8!JfR2rqBT%g-1qFRTN7pC}^GQxkjoP_! zeGRf-yHd3ANi1!GUnTWoyebvMEzV*W!Nkdr?&@%uLv@2}-4ztasP3C1j0<=en2QTj>okmCQhRF?*PUbraO$@Esz(Ho30iIn)w!bx#QlIA|&balsIgn{C&-p-S+G4>ZxbKc)kQG?|) z@{9{@EqQ8ZhkB6%xF(Dz5xN4<3Wc#qfnRu+L{Ex)<54&|4J`kgV&>=7`&V+># zu-1^&-)Rt8ivHird*wXdg}9N6%+1X`3nmvMB_T<$L&`wA%MrO>$-=PbXQgnVFYEy+ zDD2cxKP~mQy(ZiqMz_Y~^q04SML50F($*H_>x=5`?;jo!f$I&TKn4QH-)bM95XOD% z4DTbt`U;3xs{WCn|6!Fb-*ZdpM@0q90eWqy4n{A*C(f?iGnT3>FdChzEh_u|Ch%}cX9_3X07jT+#&@7aDh0!uyByO z6xKrhI*+!@q|Hc_8Y$wKPqJP!ea}V2AMb)B=Z@qZkiLZE?e87*^&MqK>`~IZl;H8OgBZ8Wu)}r{-RD(hCSMZyyHx1w5zxeTE?}FUW zp|j*cO5qu>lQs*4vP13|A}hy#`{3^r$hnImP;|^H7;>!be&eb?;I58C$&68E6X6Tr zAwW4?GQlp-pFfuFQq`Y}b$6{}D(k>_ID~;l&bXwu>#2~?^PpoZ3nQbDrMI<`F$O~~ zP#bmnbPzC+0>FEI_+Zm^K%kTS{>^%jA)npyxwA{HCzZ6<=3Us{#iGk+bjNJz>?{$g z3I?2PZtf_Os%ocHbZE+?WQ|xwlqkN&SC``v!>2E(1dm(o>jplFSM}!+ z;OwnW24BlnC>4^+YcI}Nq*|~&F0aJRB2 z%a~jb505LTq-#y21B$~Iep(2nx(s&^&;uPDpobnU@`QyUEU&dRS|;c_83{>hk&40} zJWhd@8*_`R6xKfc*};nZBx}q}gQnxuG;*dTUwlT{ZhcEDtxwvx4pIh)+Q(}TB4Ig(5RyaE`TyfP>RbQ4PY*L>_nE!I~mtb;P_XVAVf|8_9~ zuWsmx`C6~;uJIN>lelqA>kBTa)?{LFB4Xugzc}h~6wah6J7Cz~r+wfipa91W9XpfF zD!8)Tf4`7W0ax2S9}Rkt)#H&l1)pi_=peH(K3_&oGvgS+NrB0~>Q)l3A;i%7no%yT z-`+rPUPE!71(`$T#aS5%0v!G44%Dnt3+vpDdOd+hn{JMlmv_UR7Y7h^SuQ6I-_F;V zB`%u!=pm4D4V}U439chCNo{NDxmYHYZ2S2|MIXo&7XID#{T&CJC9FeyUtt8GwDQs~t!urhF zPhs2(5Q?xp2d{%q2FXE&7!2i0e)xya5{Lzbg=7yN2m%JU(S1ZbT-6CC!!J5a$K(qb zaoy()9jhg;uJUAoPcPgD3@yc~-y?B-R`|$#h;s0;@LW}U=bO;VkQnko>V^~qNqr6L z?sXE$b;J@=h9D2oL2oJ#>KrBZ>;poW%gc$7=H|CGcYAvfAD0^-2v@QqQP@Y=fC7j5 zAHRfT2rU&`Nft(tI0Fius{WY7HQ{Hr4?cpId5+!vdU6*#CtsI0ecmP1$wvH#sTG(9 zy_qDDU~+c&2jD&cZCE;3R8y?5EAO5876#V0dr2FR-jd?=$l$(EGTMGI7^4L^w_%>5 zOUqe@6~Sqq$Z9hF3dDRy1SZUk_aZM;fy#>e=l8$-7O^B90`T<09H7Gb(Iqhy2_Xr5 zulm}Z(aSc;H2n{#Juh$U{mBi+ja>xBakf8llXQAGfCEfQy8*s}zvnr^8!0y!Va5Yw z7c=tFK zH3h}f-rimioIrbAt#xMCd+mST(_il|dma2{LQgyNEq*K`_)=U)5BAGLWrqS9Kww&q zw5%-n^}Fw3JWG+QF{A*4DUZFp8G6^_IBI&Sv1tQSRlrbBm)>z{S*BR_ZcDK|9Orw zHDNScN8uuOW~q;l4vPyMg6->xUdz0lLs}lDeeUg{=M2)(mKBJP-l;5P|5ExTWixlIWFNj8@C2#vqNSZ`*ZxE63jtYs^yEX^{FogU{v@5tJ z^MtE4f~u-<*$S27gHf*BcOo=Wk?$L}Q$1z1p85zNPXfeC2&xXCBXiADM~m@Yh&&Fk z2yhx$R~A9>(ju5#Sp@eL%sRWh91oIc>x9inA9NoZZI%996;b>&Y>b2gj?J{wQ>SBF zkd1%VNaF1%sCu7=j6Ql_&rMnw4XmGQ!y)3%sDGu&U+5a?s0%g}3ulYtkQ0p4S z^xbnQ&X8!v)gR*Q-H471q~Bj1+Fj*zwN)(C$vG{w^PV`H8mHu1d}nh`*yaLmyg(>2 zDgk$YsQV){RH8iuMyF4ouK+H@Yrs0a6UMnRvjUvg8SBaHz_h9d9now#TV_({HB(lv zGLBKOTR{!lgV?jy02+9|qyv5>Zxj_(cj+NBW0iv&j3>@BE+2065f%wP6W1dGizSWT zl-1?=dtX3=`!lBl*NE5+%ePv(KVqg& z{azqbKT8G_LP(OU2(p;nm>dURI3Ay%;YXpE>rc5vh1jwp8YcU7g5XF$~?Sy$_tqoqF4uXqg;6WBIb zjhwyUYoPVp81DJp=74nQY{Fs}&0@xbSGm1LXQ4}tvUIOO&FD_I4CB<}oR}bvnNg_2ouC5hVtLj;q zctOYYmJ%{q@$Jj^>9`RNgn#$<=q(9jj4rw|EH?5{=_FlWM-4X^om8)|zFT}qhSTazLAYsomj(+Mu^H^T zSC!?G4iERejHrgOf>abA@ICgl`WX5SH&CFziV{P+IKw-Cxc_Slhf@xQ9^JknBK@wW zJ6_33^&x+dhtGLnx#ePOiLpmeWF);93(MSIZdw}sE7=#3Y-|dQ!s;tA__#l}>B`L@OjJG(ZvxaV_P4~oOQ8EyIUkHWXh%3hW4?POIczN{)!KYtj>YZ!KV zc6d*r{Iy4ahK&4Zi6KQZf$N*(<%WH(Wt{C75Axz_Kf_48Usjel2H5m9`^{X1LgAI6 zyr?J}2(w$hQ{!{4%_>#w=Y_;zfVoZiPUhV;3k6tTo=tosc!CqEio>aBBV^{LTxEu~EeGBtP=V#bg3hrE$gX!- z2$vSF8*IV|d!avVOTyrvc7CtIm)`NkVu<%`iro4rU#aU!YxB-{pTx;{Dh=DPg-k81 zO^otpEVV;?RO)24lKf_$9c?YERhfvy6o-BcA({s4uaF`g4tRq*Pdht0LivVNbor`J z!ZSv?r9IJzNKR&w1;oLQF_}%r?wn!pkBm7QFQKsK)E=hDkInbO;YPC?hvlZ(mO_;K zcb&?YQ~4af&Z^orI3i{8a+83x5Dl>&fHh7UYFk#sJRn6lvC~5B{iMg%GdkrqOD}52 zcAu5L%813(Fp!W{nc3*YU%b6HDNAgzlkXK)qU14j$idH21IaBj=@~QCiNxq(-?2yU zsN-3UiDQS)0|UW!&MUe24mHW(kYtJ6rILh4@NvNa(}RXBEnq91Kf&YrVAF+c=lSQ$ zpUprv*Rsw7Ed^G;`+ClWXf5+^vCC(%+LMwqXsjcv<- zqrqzMbU@H!4X~7qX5Z;b6j}&|F^ri=Zoqols&a@R+uV1h$ zR^2o5ltuPjme@Tj`t(U4(U3h*P?2?bm3-Ojg#_pFi@PD+**^Jsn=bV8t8=DW^DTL` z=NcTmM|BhI0vpxwe#r@f;vI3k@;aSSnl52;tuAPHpQp|G1!{f+Y${wCwCWfU7T)ty z%hYeo&3TH|ii)hJBE{2usme$#2NBRX$qsEs7~TvM8rSF=(e?b_r^$fC-s9v>G+ zzG$<#z}+Fi;u5&r=UUY6oLkwpRwz=#(vCl@pdtLce5FlvKKYiv?Rp*y&}$Ex@@)ju z1D?2cu5=a4uWSN>#5HYO@nVZp)TauQo_a)! zuGrG@HxJ}hopbg$FCOdK@}TxljxPo{ZkBwx#}@H$o_EOp>KJAQgS_>_;lcF<+x^PT zigxSi7OAB9&kg97bk|?&sDV^}RYos=#QTO9y+eOk!BF%brusr}m6iVcJGO|duhtE)}7iS;IjbD>m zzdkk}D(@*OcR;EZy>40blnBr0?b-&9FX66d6}QzpmHmae?UZYnF^-@Sn}d2+@u5)| z;hf89gj8M|8V9=7fI}}BDKIGE5To?4-kaVq-?F-^mz?1Bs!nh#E*_r3D1G)*;yJ`U z`-7Ii2V@OSoT>ou$_y>q;lF_O1yH*dZJjmw4)pGC-b2EE`B)Bl8P_SDxyol(%2nWB+;+*l`P=9#TGQ055t5U7L z)1QDvnp{VB#J=OxlARI75MPwWPA&fj6pm1vQ~Xq^0fv@{o(l$%T8w{ z(XcR3(Z^hNIyJYMjE8BeTqtY)w#^-D?tpU{nAQ|AL{0S6PJJZ1r)D?vGE$w-Y`&}i zEXKrQHRtSfB-9x*T@G&MW*si0j-L~+VFTZCWi^_rUJ}I0H_+zegOrhTF)?GADM2Io z{f-LfgASGIj^{}lE$Ia^+JUNobI!lh+u>-)<^CeLm*;1#z(=_prCUK`YySaQ+2%Oqh-rE&GFHS_SEc z+#gJMs#i0ZwYFkAO{(r=+Q8v|24mfQe-H}}fp&9N^jk0sXf}=L2~qBYXa(&$y zRQ*#rxPJz7=_Z$jkLE0Hw}gg=`_<-dja(aLemh5+jSZhykX_+$ABY|7w*=-nPRH4d zQd!L3sa_9|QYtYD&8s<=BXl{sF~kR??7fv8JMQFa$F{~XD)3#L(tzxw9B*aTjH~Mu zttWfrd|@*hm&ItkcE_{ziR)&yzjCu){_w48&v`Rrt!wTE)KFit8+W2|t`Ms>U;PU2 z3ySgHbl+?3T6`>>rOLtY4`Z@+qPHui2Tz@3y7Wu3$M8wgthv7$uExzb9;UVX`G()+}-{LG#4D2``-5s!bwQg7jn&TBp5Wb;{aGQI&VTMLME4@?sifq!V_ zX64j6YNc+mpNCHHlGURwpZNls54B8{7@7lz3oaHXc$Lk_Yn%pXC=K!I{i>Gr=0%kT+ zS1kmBsflkFA)- zYM6pxc5*1W*e|!L;)tuemBKpgDItBX$6WQMvF$hon85c@h1cL~d+$Q+=>76?`vtCH zk6hZ1{`^kux`RIGW62F1%X`@wL&?^WR+-;mn#`LCA4u>X2Xr~E4_%-~o;#c8_-1cE za7OoFJKVHS-RLhaQE9&CoD7CdY1+gwAiYbf)02UJz{@*vji&gV85T896+IQEshxq3 zf~e*qP9CLQ&FNxRKiBmA?u%F-uMMI#DI}$?PhWf+xbJCrS@gcRrp8Z8jbFzJ%0GmN zEfY!=E@#}!&OL}Wp^h-KSqyyP6oih$xeUffl#37Vau@}u^7{DPkF)M9x0u*(T~XBliXMU z!Js8H5n+Mk)?{kIs7ORaM;jtLb`>w0^dv^ov`9sQkgM)JL`*MWK3V1i|2N0DV=hM} zJVD|~hcRAq!)+eNBap>y>?*`T$|IkGmOZOF=rVs4I|GmoEk_hk!f;@YfWyV$XMC0F zdYE&$QDe#C%~AP#F(QWgzW7Wp_|CVSbwiu8bi~J*)dTEh>{36!6oQ#Ovz-c*S9AGN zvY4T(7BF}{EDkO$!+kN%x?wfI-SZIFq^obNzo@dNYar!uSHyPKc<-iMrOIL7 z84WiY5tFd6KcOy$<9_I3vP$J!8VBTqr^H5xnIKDn$HM=r*F)K}Ni3{=onf!z4sep4|;r@0b%(N!fgx`a=7d2XTG`Y)%zIMl< z^7zeeSLh>n(}G>aB1rk78dV$vSz$tv<;qDLs``s4&cWMc;xy*vBZZ4mQqNC@_lHy* zJ+eAwy$L!#Y@5EY8VEXBddrbwGQnMAqe%cq!Z3}%2t<+BH{-#oSFnOr2Xn*YoVV}D z_qS%Xl40Lvd%ln7F<`pHjoKG8QaG1J0YKy>9C&L)yN7 z<~cjm-Wa!MfBP86dEkrLRB6#MmCR%~MV!;1iOY_+mr8?MND2QcR=PMeq#5obNiNEx zx%n08$=2gB$ym>TUdTS%vSoC<>$%wEg;}Q4ftoYQgoKatVdea_A(9D4|8y?&bC_ezSp2%2_jiu~57^${f!oiiO2F-b216a}`%?;g3#@gHqeN zY6p*q`w^Nx-qm#pe^K1v&HOIJe-Z`QAIHrqIzcrmKSuuJZ@U)LkBps)?7HMTB6-IU zFg60Ab)R5gri_XGyR2rhpQ+(+Q5L$wqamugdG_ZXIFaw|m*ZPb4x;nsd%QMEAz?53 z$F1QnIznur8`${jr$)K^3Z(!{()a3IS4nMM?MR6b7mcQEJ~@DQw}>Yq%{+<}7k-LB12})!$4XQ|a-d-944Z<_0Ytz4cQ(a6<*Qe*_2C_klmR;zs2KNnY6dZk!#U#A zP=H7$ESTC%A;Zrc|G|K&BdXLaWlaBx<3`K~v+LxP#e%}39V*TIz38(hPCR}aUzMt} zT*s8Y?bukc?}T-B=PB|j61I0(49MB)L!_epj~3Lzrt3aA-L_8U@(+BPrc>5NbojUyZyT_8iD-T zDT+h;&OFT;Q3Aef`l2SPWsaKpx;2M4dP+iHL{DCMXF|K;w34CHk|Z2alm^SV*SCRn zZsU=H0R&Np%F(@!p?KLNjhEYzM;`?2kU^-RIlXE*pB1@!m>JxL{2GDTF5%-wACvGX zn_q|ygu{A3aX2eF z>SUSl7|zCd55&=f>sRyA!!{5Pi@+ z%WWOTvq68}#Gx~178%0wCAan{NHj%$v;CoXp6iQ;2Q6=R)`!%S>*WW|Vt}7{S3$P2cTZuqx$W+(>W~80Omc9_l$(<1t12%2#GKg# z$USSTr{%US@Z5%J)zuQ!Ppjo)Rqrw>MkS|=+1LDZPoNAca#?qReB7R$Z>8{YRU1qq za+$;8n)PL&aY;nJC-o8*)ir8P0u7b7E4kIQy|4>gH;kxfhEeHLrSQjz{X%)^3uDc76_9ZDn-ZvPELyuRLmcWo=`SP<8_l37gST+i^OI}fguHO}ok7gy-D_F!g z@7qo}M;r2h;ydw85NBZZfeL^`*oJlZ3G(YD5w*$<0sY7*(p@>DP(!cIknvLjM`yaj zTUNY`&pQon(V=x9;Vv(WjdQFQ9yf}HeYln)k4-9CswwJXSDc8}$Bk2ae(uG)77{ET z0FO=yi6L@1@*if1lsWHh6c+JyS2(MWdJS#D zr5>n;u^Xe)=0hBAopP=3S+1Xew7U$4xb_R`7rSHa>gURD?;y!?z%nzoEpKOnF>w5x z{GhtK@a2?Ax(|UxocmZ-p624{jH7(@;8Cu2$5+nWzHPvgfIrrqdCan~hI;(HP(OEl zLr~0k{PQD+{fAw}InBp7&&O=m#Sm!3K>3MkQ`}yu=$eOk%^OM@Ioj|nhE3yALz}Nu zD&oOs_-sMwPw(kBkr)~c$qT~>dVOAToOqd=klp{ipI#+p`5e*?It5xMrBiL;YEC+N zX=yI~L7v3f@0dGsSM%@tOFRoOAvc7IhL)rLhRzGTLcy#{w>!*L ztyXkU>uom+)(XzkFCuKpSk8cMP?mV0-%^p*Ru^x>akGn$+0QTKHk-!&xuaJ zaN#?#i+Wnp3^Jp3_v?hy2>@SSTf)~gn4`3Q0^aRCx=0EDyuTO-q1qggSY8qC=(V%l zxhq|$gXVAf*-gM22U--$L*W>!d#t^eW{WAuD_Og-;I>oo{=^|)kQX66`c}O5o6f_KTaq=lTY}8i2HTeL=9*q2 zSpc-F-yD_DspeoG5nH=CIcA(-sy3tqTHr5$C*s=cJ_s7rI^0c+EL@0)CunS}p709W z;q$tttjaOen-QMTU=I3QqxLyn=8F@DW5?%bCv8F|UBhe7ceUGiHrpTg^BLnDk^*T$ zHDZ;nC6bd}F%~g$t&b(~+_s&Y=b7swOhhnxj%IfY7{)asFlb<%2Tz;snhvA-BJ4V` zgI+Q!>Ly`s8;mCeQ)*+~Zs5O30RZ~Q;W_g zc)SHi(a!qK%O|~_?|b3K!~+_*&Q}cc!Z&z7Z0OC9{v+N zyVZ4zB0RNW$I)oWxX}lUgW8n5Y~}p=*&@Y$WuIu4-)I&nAgC#Bzu=s&3IulOATQsUZY2s^7(?p*I_w`RcLwabffX!Fk1))jtwt%tTcEsj1{Az?Mm z6_Q~n>(>!&IX@@URZwYYcCz0i2!M1QT;nwc0jJzDf|_AB5q~E7g{%KJ=cjkIwlYdd9TZMnz?#=}qvBTN+tfy#j6Obmp zV0MqE$=g^}oCG;G;X|V%Jv=6hY+iq$a#xe#-)#A4VyiKHs%71FT=%f2#OYwUlI$S= z?(rJ|TqJ)0A0emS0pyY?}pvQX}c*o%9f1$wRqdGrq0;P zxggOz8*R)L5qfq!d?0fC_HMk>N%?9OcVQYVYKQQ{!zxi_SO?zwQ$2k%WOlo|ZUHnP#Xd7P^oxjpcIO6_BwiITDt>VXpJ`Ad)?gr;jLl%s zN-o^4qrIICaVxByJ$MxG3rNxM+4mqgR=gJuyUUE#a=g1Yn9#1tYQ0Z>Nh1%YKJ#>> zwd0p1&XD21xIXRDP^}0*WhAz#Wg@{fq&E4B{AO z#;{zPfDQi(?1Bk-ey+!aoVcEcmdkv_@1Em{cb`Ml#f+2?gJ zI~6gF50_a5sVq7ave29|6kSWjV6n!fI=p^FX2VRwYL{S5kIwW3ZF6~-p$3Fiw2oT0 z(4OmgKAotreuy5=G%LUC*eBp`y@BG-$NVJDc|%)CP)h=o_i7q_!rO|SO%9N@D%F}J z`9~1cJR`VC@=@(K0=Doy;l!^A0}#&AAq-gc*s476-wyRFKo^WVHAw5FQpO#f^386j zuQ9<~zwcH@i1H55KCuWL9RpvSyUY_huC2m55@ORebNCbpPO+JmT=404o+6^G!|!U_ zoA*}lK7!S6U?6YT__nH$Xw4qOExduVGQ3T>?0CkOs}Vh3THdva`J8*azl%Q{RXNta zN#9~51t%O=we8G~V1tUrG3W)0HEC5EAf&a;FC6tUAF+;orwW!zTr?xkk?(2lw;Wij z-KAfm6Ah!K33Q)PZJ6z7o#kDzs+^)}$4ya(=Nv5GUJLQft-81@ zU?JUsiqeeXb!qQT^Bg$H*C_quV zQjV@RPoamv`h8C01ufHqU?ZjnX-`B2E&Z)KkjMg?KHhLQ!|Ap_R8UZ0cT0$T3Ajlh zz2%Ahp8(c(N-%;Sry}ze31f=@~L&TWt zcq`SVI6M}L)6B%Q15zEzc(}K>4@kse8m=Q}{enCJ^QL-fHkq!lw(m4y1Y;YtDXDU# zMl-c{-G&;vD{w$pa7v3f;_)c#J#K2;3!+Z|i;Ia5mEUo2s5o;;7ffXWxWn52jXN|$ zQ%~Qo8(M6UXW>A2P^o6T@*~M&Z9r^%R;VKeALgA9iGzYoOO38-xmyaokL z%Z9pPuXh6e2p%p_2LZ@s6ae}CADCC9MHo*9FHuavO|riJujy*7Y)mmxI1cC92n-=m z8q6D60TxDPW@adJ4@@j*t=rn5_@8)E%nevnHrKUv59VXY%P%dP^3e`IFBt@YLPVuy z?usNHBkWAR;MVE~8R`=s`PHwVr5ux5^8w1|&d-~zCuIs)I?@zwGl=!~e_`z01d1`C z)w&5VYLWHCTkbxlFWdl$Tf)ZXB~sKif9}p2Qj-aQuPP2hst$UBFlDL?s~xO`E?ySZ zm7?Pm+pKd#B)UROIgyiABi>9zHc~7=XU1YC!-->=qNzW5@&pKY{y)5;qzX{GuB9)P z^psG-E*QaO`~F~V?`@*;2(E*~_VzYlI(;A|qlm6A*}kdk`O^CXb?4x)lq{)-her^I zIPJ;J^6f2iZ{NPXq+!#W?V~xi&aiK|slx7M$p;;8?Jj@};toIoJAm8+oyk#f`u~o- z{=|*Qc<&+wfUwr%|x)|3}lyH*+8pC2PuF~2^5<}0FBJ}8Yq@e0t(#!T6iY6nMsUVx>4F%`f=JqAi3OV zxTD>%uwG!T6u35C-0ocZDb`e#UmAd?1AIpVdl|_d8$@Y+D2w?1NjL$I&_Gn<-5ie4 zbE_9stR}CN(2aYD^)@{`p+RlRjWy<};KG6eQc%CwiY9xer9}#W{F?t0$nU;F2$1l6 zKaucppa>AbsPRTiOY2!S9<@J=XPUpteYY8SkC#_qdJP6>6|y4Y-{JhG0M7rMg)vC1 zg9uFzK!?LI!7xuu>_7Mr9+x?s@6afy4P=y$SkE{5Vhn(CKa4EpQ{Z}r< z?|*v>iZ1-7RMf=2`Y8;8sI;qx@#E;z!{)drK=n>F!I)ei!_Ius@lElwf=%QFem_mF z#YT1P6+#}^EGeMJhqX`(A3cDyJQlrwNX=AAvcF31vF98zXa`7Dt)GF)M%&Hdn(AlR z^a}E)&NbQ%1vc70mGmGl1iXBEQGLICdjUSXUq9|@q5ZT+JVS{>A@P2P8sASo-DQR1 zUAaKuvDQY#zWq&tJ5)K$f{?cl0fi=zfak-v>p?*c1w#OStX*m$gM^^0Zs?Lgt5PGC zn3R+}aJ~=k!b|^yYg-q0$t%6Y83EQK8J6Y3nIYWW$`4(KQ(5$@u2G2z!778+CtcZn z`ws8u0io#Q zK|RSfW*iYem;weT zmMF>UV>bGd6sjvjekjWjr9u;*Y}IfYwxC%uKE6}F05C2fc`A;T(B1j|=NvPyY~@(U z)ZwvK22m=W0R$o3=aM}EB}z_Acm?#%zaJl0-c^iopE=Lmtzl-gitVUl9J40ss51-O zFIFhj2_0R^?3!) zt+2nyW?Npsl5W=0c&CJZ4c7O$bEO$G@O}cd&5On(;CM%`SC{B1b~(jk)J~fW+mdoX zxjeGBU_WvIcA5Sq(%s4Gyx`?=0aacAEh-Uv_V1b|CK3ZI1td|&BAZn|0u!5~)TmLI zg;eg*D;*6$T=6NT@U|lt^Lm_cFUdbO2phBD;oRkzk0#z{cbw?=WJ7Y3Q@}7Ifxd-1 z@11V9M!?cd23#z_D|;>a3bT$<(6YlprrS}_eL~R3fr6}2L{_WpW1`YEm>JqzPEQPonoj~2v%x=COKtJDU`&?ZDKcb zaDVsA*$Yjztfp%y!Hi=Q{qiZgvAR)F(V$eQ$O;=NE%lHg>$~hYZEeMf1%0RYKLuZ7 z{{A|NjGO#}y0E>S(Ulw&5;82B;u<4Hv6UX#qPtwOeR{20wOxkF19r+$+;rh=j!0Y@ z`T5Zqjs1pa-rnQ^8ZP1ftU@({=J(yOK2!@wV z?s6N+CV5h6sUrBrvs_*@1StO*P|YGxeLoCiV`H~w?t2uBoZhdyq>=r&iUR2x zImh0GCjN?viu@gFQc+tBh?O|}c%WN`Bm4Gr05y`c{(7OH;+5xry2Z3&n z4Xg z%Gj_7)Dk}k)X_cnZ{B*Htn(284kxs^J@Rs^GhabxUxPAA3%~%2)pcj`XwP5ItZs1(J(JffYwYitM{0&i&(=1R4O7@C>@NYNlLR93#n#(1i!$?%t*L@N42IsloKeeufF{qsy+AOJqFG1g{s zGO4$ppAHh$#qco7zdpr}UxcHY?ym!E#aJHOn{`N}?2CUp**l1UR0f71J%5f2>$}rB z{5O%5%*SBOp?3yr6L0~U88hKx4gYbFuI`$^2wW$oG*oy34^IRp$%ww>Z|Cn5l-$V7 z!s3CC>pr+}`maas$B#{igR6VVgcMAJMlb(`Dy|HO;vjxGz`bgH{o9wGBSWP>faWO( z#%o{ysSt7XCO`jz#Q|yxVL)2~WHklex4@~3i9z|>0sszueXtC4eB;s+m?T=JlD{3f zP^AB0N(+akTVnm?2vN$YfB)*;TV}2a@M`(iWvz9T!9E}V?G1naKTJJn>H!a0Xv?>^ z(f`+(gXS3kr4P^?4VcpBefboBu>=UuuNcmvKwI}yG8e;NK7K?7xY+Yx(G?Y=NJ&Yd zxfmXda#boxX=p^jc>+TbFa3eV z!0_MfHXQvYSYPPY0<6?f;x*yA*79G!{ck%QLQ#h)J%1yg;4k+uQ&zyoP9lc02YY+) za!|ZyKxQxD^y|Mo`Lqo5h5@EG8JWlZNj{HP-hX+Bm-ImSAq5W?3kyq2UDh7M?9pG2 z%NFKvv0?a?%8ffo@tgzrx*_JRmfb40zO=L_`csOiWtpYlU&s^?!T%n`R8( zbA+lNfa08(P7{6Q+y5V2LAv`41O8v7bJP(p6>dOQ3{~9lt+TMS`TZ9E+@7tcR{}Se zvcIQNnnZv3JW_;PSPnGl2h|LqYT!1Dnm?A}KdyPBuo? zpLsr9W=HE^9{iIsC=1L0IS~+zfbk_vHh()G)F77sL+QkqT59qBUk-<4svB@KuU{tu zg`k^tlrn#}Rc5e2um#i$62MI?|LaXWdhiJr6|{^1_Lk~C2ggwMls3+|nzd?{(`xc= zC%;PCMa(=f8bbxSri0m$yqwXj(~>? zOR@MDt4Ayu_NT@MrRblkJO6klNwS}Klgp8N_uZt(R+JI`d6<9iPuuPPTOy-v?)8Je zJPv{)i1mA6gdH5Xg{t5`b{zlZSk$?~rd<GkDgHzJe-@^zC z6m$LioW~!l8bWg!2m?VbaGs*wv49E|INNN0J=<>3cpt>VP%vY0x;vJc#m~ypzrV=* z4dC_cAODfC{@$2hR~ht=Kw@x&DqTc<)0j8oxJ+&h<3M@9EXY z+KyA>EiV(U)*Azurfa}1ecAAAiO%_PR)6Npc1;yZ+J}&Fl%4**uJWkSga!@f`5cj4 z?LUTo8d8fNwwR1&%T^4HEW9i0djrxEl&1^V$&p&mu)>S4$Cat8of&}9sX@i&l&a1;O z5Se7>$`ZoWgZVeL&UW?QDs!9myAPu(thSxJeSP;{HUCsp`p0F&M27>dqoBY*5>Sc< zM?w(jip8Gr-@;s4xr1rOe^6;!6Y%XD>fuY+NTS^n&T>fH7sAE!v(orvmud&FR&;Jm zfTPEi(vC-X$mukkW#KSHPq+&3GMUQG9s9Y~QRX`U9ru3-d+VquyY+pXZV5p_X+cUF zK{`~ryBkEh8M+iu>5>+KK?H%JJ4QfKKpKYb9(sVGeh=q+-t#-3b@YALnm<@9hUeLP z-+S*nuKT*=0nHuz*_KZSwL}p!&Y?UEUf7ntr|`$IVuuSYPT~`P3K7sAEtqr|M*7OG ztgWT5gc>6(;1FSm$j0wZKc<-$(ZdbW!^0;p6q`!`(pP1to#}$n-Q?=;^AYo7{J}W` zw1H2x#RWp5I-@o7Z3gi}ohI3XF3Nx|KPIJ2#*u;`t}ZI~s?6ytheDv0)4DlX@ZTBb z|D820Am(}jBoq`Ba)AjG%L_?+K`5dDRBUv&=s66l;<&`K4Ym-yu!X($y)7!}SN3hm ztgd-+g3*&dx{pA422dD8B>+?z^0ol(J6W#&)Pu-OclbGt9}_dPyvf}CSkLSLWIUCN zfi_BgQmeVk^MPFI!@|8gkNE^X1+Mw@^!Iu0g5)l|+z|S+D;dr$iGwX?w`G5# zAw->OGbfz&z5M&Tl%!8UvpyNKS_UY2KzkYQNusJXtZLADv%-2UI*zVHIm6GyJLIWj zx@|1SZeN4v_G$!eV3L02dUtlv)#GXWT15i`QlXu)7puiiHj`~^b6s`_DD#N8dr7Hg zW6g}ers-6bZK4vd^ia_Cs;+*sU3H5Yx^Q=LZE3a@*zVRcr4&G@IXtZ+ir`hu^tv0`AEaa$kY8 zzPhF0SpEFQKyiB&s!K4%O>5_BoX zzV8PeAChfM1jWft&wxgQ2LOy9UkLG_WUPtve%!1l<#En+fu=;+3A;|Q-ew@42xL9& zaGa6y<*$3lU#;suniY4}o0k|gUpAMps=))qX7?aBSC6Wd7dirl=?40130O`7J?70{ zn119N*w_k8%^DmWOlgGEunQ$JrA)51EPcnu>I1}()(o_V#E;j-Dgp4b&+6F3$^r8y zFXJx1FDA_mxi2z(PNN~G-N$<^N!YahRWFIVh1T&0B^bm8#JcoX{)?>n|F{NU$xt~A zdCc1|j&jbQUeIs<%V+IPoGs6jBNVP{?t0^NM||VO8KJBf*=fYS0XMSIxwXvF-6wk@ zJ7UOJ2~defE}Zw*e(`?RZ|O5LsV-3YJ;2)ML}KQY+JAobR_~%-#|NA2*c=(cguk-w z+~T>rES4t~m2KnC#oE(@ifg8|68Lm^F=iV3{qI!+KDf==dW|&n=`Wj(m;uEax%VfR1_WSo4{Q%LkD{cwFNip+erzX_ z82$5*C;weY0bFjcmubM2hDNbki0nnFTSgeJexZbrn-aB2TrN=CP9PTU7R*kgga`Is zSDQ2Vk1zhhz6Or zug5B82%?7JU7x^7Tnyo@Y{shF7PWRLa(vI4m_0}6YkAkxpriEG`L4UD0T!kW(Tf6t9`NI{65x%lnmxrafH?TD6A6a zoGH2ia5c5crdxRBYtslKKW&vg11dcifwP8B01+VMVrUub&W??}B3XJm4Q=3Y{4uSX zk!*+1`j9N`<3#t0o!s%~t`Mo*7Z%D_8v%uewK22e{vCka%FT;`9K=bvEDaeo-p{nl z-S&z(zVvhVGmjLuQCmZx01Vs7e#_$v+D zr5{r_D0+H7v2eBWj(P^#yy}rS0@Raj(*A0``tQT*cIM5TZ=-?#B+k+BG-gSoK?XOP-FnEJ=h?Y)B&(eLD zm25J7RNk}WF~50^{{kcm$FR}8*s_0Dc&oU=xfu}F6*z$IWQrO#?Lus*>WTa_?cFE5 zn`QcgVmG<|!n=I2JDI}+BmkozBF&H9mAZFpJ^M?_>re21D6j3X$O6T|P{66uu^lR^8+gImnABiE+cJ201mcs? zO|s86>xNQ@*`;Rsepn|~u*T%}zsCxiRTJW~?1};!0EEBWV1;WYwq20{9cBBT60J9g z#8HUuU^A=rz{Nw(Z$Ht0c1mJ!tB72&$HEv55~b}cy;URcUoik&JVTZzVxW2YiPQRV z;GaNFaZm|}#P9$9n5C2acZ%Cf4(P1Tm%MoBTaaWNP>P^Q?-mKpspbv5Ibn|VB@<>; z_kD#Qdt`fHbJfwrcP?@n2T`54@D()HL9s0Ec~>`LB~zzLB;nB-uv`Z@;-9Z~XU>>f zc!F+e3Jh&_a5Q!<5$H`g^u&KWzPjod5A>93KiYnNaR|7UF!ORkksURm&6N?+m^&|l z#(=Ql)31C!HpPP5L6^4SnbXV9ZH8K6sCcvh504EP&AmU2#?18J#IET%hlL^PuS{>l zlf$8|#}Y#(Yf?^>Tw6XNq9-esrTlY5nV#)kv@K7tX`4j)mH|IgA*FQIsB7bA3&E{5 z`JeWa1AJMFL!A}(Ls^3tw*%o#%L4V~&a|jjuPzIIh8StM{2{5L4?FA|iA4 zN;Ed0nhBthIPtvact%8czjkw8SXLF)&J};pcbV8K#R(KmrPF>N z6NLcYBuYC_ZRF$jc!b^_ar{L&V+AS@;q5S_Iwf(q4_Cs8jecbgg&K;9r$Msc-OTSv zOqVx!n3_tb3~mn@e+t}kUEmEETj1s|Ev| z5QiPhv2x3ug+MxfYh3Cof1cx+PZgv%7*Kqm1S?Os1E*3T(rODk!Msy@L>`@3mU=`U zh?>@65FmU#1j1NGn_++%XF*`XH|u;e!Wn4$dLnVQz))YKY6>hg5+FAki%L8%-G=j{BLn;1rdz0y_kWg$OMx=p8`dqGMw`@}AiP zXqfedE&Sway<{2`KMl3d7}x=V`Ez8Stw6WBj^y{N&xdwq#d_r$gc3^>r!SNqswZRy z=sW{TG6~;Ludn5s*aYre#|hp~f)H+)Z;1pm=z76G*m zv`4w2#5nyiUs*n2KGBPNluM`lfesay|Hhy6RaWCvGsqdPOm~p6E)R$Q+(`nxvH`9C z%yfqkMkdWKVACX#n^2@MWT`y!ND8<>*wcq`G)cZ>2tHfv$_D3Gh?$G*#BGjMW&kRr zCUbs4F*71&W`rRj^>aq#Rd?EPx#^-E=9V7mH+jF8*SQgJ01^SCMfp~*;2LdtuJoZ4 zFDXP_f<1QA9jnA%(;l!vrhbql2&u{iWBl<$hlO@ayq}(4wC;bpH$O;CVZturvBRD^ zSv$MUS0Miet6x5_5DOuF|n={uWrmO4759bXtNwp zy_QQ|A&9*DsWo!OB!||+O*jF?vOm3g`1tZZNuMJ*j;GA*;v-zLpPum;6lFwi3U6fS z9e!4y86Pi$eAi{cm)Dm^s`VQQHE1Dn#JbJC{O1eJAP*QzyDRf(><7$47kuu}8goHZ ztl_K6Bd(RhCy*cJbl5NE_gYVD<__NAhayPuxX~7^8Sm1e{^1isfg@>V9_8JRg+-+; zS0DV`+hw@t&I^BftuuJWc}&)uI4C-ET3VXMUiY#a##O~JbMq`F>fX=cFoZRP_S(!16P|1eY>xSnrW3PP3K6E8|KIHTS&`E6& zG>ON+W3pgG#ai-F24`$X4;|B;OI2%ZD^AN66M1=qm8rN2G7llbJUcR|K@uRO?Hy}F z8JX?@oLUt02`t-oIWz+B@?*g_t<&Ze7O+FKR+=n-Zk;BFMq+NTJ zgDr83s!?&b&f3A7VDLP3ExwnbQGjw6F{;eE%0yorHkOcU|M%&=ri^wtR-1SxpJ9yn zmYhByy7kn4ZusRyE4*Uk%&q{}D|6BbQT%N6E$Q<+>dteQDH`#~rDH(9tV!KULL@li zuF)tYC?P8;Dw|tw>8a6Q>r_iB9HhQKFgRO3@Q8 zaxO}G#lUM5pyID|7YUlKaZC^pGX6w);!h}rg+V@H6p&m2adnSzTUzot5>L!<-XwLS z?wziZC-qqf2^lUigv1FP-!-Gll5^Y;V03nHT`-uwD)+69dT^8*<+pin+K|?A;s^hP z5$lF*-J&CTRjgXDx#lttZXC)VK9VLRsl3)7Duj$Q2iKoRi-XKo?rq|1KBp@?hxGSu zcuE%<6>7Xw-@!={{{9*Yv{!Gv_m~X%s-D+i37vXRwvPT)-JsDa%VDNWJ*7S&y$+(@ z+>rN?Wnk)@3H!x1F&K&>yyaLtd||Rc8jN(%=R z6s)~#e9_*tNla#6{`j|l{TmaEs5?=2A1iG9K+XUR!|wzC@f+s~bO0`94;tJ_FNnu| zcu#_l;ZJ+^uiv|n2ICb16#0eP-##X>X)D}K>J7riixy?%x+49LtK!ONMtRWr>cb--(__ac zy+KHjxlBZ|@U#BlP3ysk2sdG7dn@2UxUzF5KKjq|{_w*Y7jf_%g0FFGnqLSU%`V9w zW_mLI;ZOg)Dj}hgC?ec+&|s210QCfivfxB_0=V73U2I8nW)w;aR#w)8q$F^)q;z9#K~_rN?a6&rx11U4=)^yahNFyU~X==T^g_TaVIVu+HJXlEl|7S z%ITBhAOFZFML?73?{@$yad0@ZWe24I_W}#_7lp;m>Ffu`(AFRJUfsRXBG$__vxkGF z0SaR}JGSexc(OY3aSY$c;*;9%uIz&!X|!08u5drxt9eH*NS$3;}Jq>+6sUrWz`{rF9!;FNJh*xuTxvR{+^vQ;+11=3th@qW=8Dy(cpFGzimBu!#os z)XhM~pM7<+4MwLt)dBI^LjAl3n4>zIWdBv(4v^g(FE(Zd=;X%Y%P8|4mzS@&%y9m} zlD&!L6cvG^yO%{-%0-KQ6}6}D+ujq!)n(}*AIy6=tu!|G5)wBbd!(g?^m=<5rqv5fEV+D2;6ru;Q4bjObV&=#xfkPvemVB zzMi={D(9-0OviuoaM!YHH)d1(Ir<#AG}DY$)5rhm?akmQ((@*X`)L;OJE zloQUH$IEsU^Rj6u=V;h%R;(Jllk(Jvf6bHmx5pj|1^~vj8V1X2G2gnIZtL{^LwrE_(A}rn?9ls;?{Q1g%?GC@2M^YQdEb? z|FXvhpl#rtDqp58%-JEy^0f=N&2r!yxOe0?c?$snYawH@VuC>r|8fEoDgEz`3YOH{ zEvl@XJe;q%jpu?Lt}5a*Fv+k>V@fB&dwSj>Sk>H@%>L+&;r$eiA1@Ev)}+BOXEgyH z9kvXLG(YJW@io0d19}0=MX$h3O`GnvFbSB+fUCvO%R-Myb3nTD-Yua${)yFOW2Hn~ z$Qznr&%N>LM34INy!H_6ArcA+dS(1sGs?9Z@n_N?SJV&DJvWFH;{rva*6a7YuZ}e_ zzaBpd%NcZrRq6X4Z!rL%0z&iHK+uVHO4!-Gx1>0<@Sp{fsb;_0%s^js+Yw~$*T}1V zhIZ>Ud*{t<8L?Avrmw=)*O3Lx-(HYmZ&0-5Fde$ua)pa5e&F@OrDubyW1Op2TT9~r zE>MLgZTZ<-?Ma(~8ObomUkg_CL25U3lpI7!J~XlqUFL~>_O6ILIr;5%Qqo44S6yQ; zxz2;G&t<2rrFyo&_}6k#|5R#;V7JN|r)W&5q|*Sg>QuxiWYRt8nEX?7YDpm@=@7{o0WT7?^B z&o5k=#<})`+?WM@ukp#&qi0UO@pN2ueX!bik&Bb}!goym3KIU$z}5C~%|4m;CIPqY zGcPkT4ud2%a8vq8Y=7D?VhGbj3bPHOF!ktA^4n5Fu^Opnk=CV`#7p!AT5BT9q1+$H zgx(%mIp9dnt)F>T$^reV$*iqFHdy+EGZ#|5RT+&t2 zpl}7R)8=g58E(^Rt{Yul&ra#SljR4P^tuyw)zNR>@n>(gFfX7Ox=BHY{BcENx8lb#l*Ad@=Vx8yNTy8^5hPOeAShzo^~ z-lXcAq>lpYG^8o)4yc_wARC1rc=w(U&o+V}?0Wle2>dL-mxyTv>hWA@KA_?i9OoUj zi1F=XG8XxK7G_vZO!+8%HwOFg4Frc|)Q+ZPNUnt8aJRXGiV0i_3)kR=KY2 zM%%SpYC6=b3u-{&74I%sd5In>IX9iek$v~^2XeVac9WeA+24g_aUy6kVPyA%?z3vV z72$5;uJPSBoGNpy)m3Xbf3WyNoxism^rS|z19T2 z&V6LIUI^B}q1?k*Mkx1t^Mo4Z>2K6yjLxk>)oMivyeF&9DriznX0BsF^1<Vlx-~vmp#Ev>eG{u4t?@GSI))-fxp}4^V5^>l!`2rs z?{vjVT(JYWqy#;2eHEb!yI_O2v3IqCL$pjNE-H|I(#&Twe5yDO7IoC44gOdCpx$>! z8v0FEX^dp@YST$}3P)?o{4eB?ZS~Y5*YW2^tqIxW%gB6<7JnaW5IDvNwq{YNwaJ|( zyvv_IZdW1>cbU~z(FnS(ml|eL9a-lv6i5MxzK3xuiOlxU2}nsvSHGCdGSbsGirP<< zCLR|~As)_50Q`aB#YoHQ#Pwn8)?2aR$JyUnmGWWR^!X-Vb=;IiD8Fy*(z3WuozYnf($t@w$LuBs)GY8_Iopyb0MZQc}n$3d#{ESq0izOl3N~&ex_fEA&n_Z}l&xc&%4bEn^avW5ha- zXeuDFH6?kr-&_LbINNWrIMaLgi~Nw}=h{Trqo5KyI$w}{$Z@kULLj1Hy&^iFK=w1zxp@)V}V{Oau%Wtk}bn#1AZ~_ z0K|Dp1IM7~vPb>-mmOm|lZke&bb*7rW|u8)$t@G*q=c5HVxFy8Zij#n^QMn4mHYa| z*u;GAdXc}2u~U;t??;a1nA~mti)JG0dtiKsBPM`O3Kf z)2Uwl9D;8*^!W-OHMr#YS9U4~;==E=~C(;H&OTo194H_&wsP0UFo=i zG>N>j^4QVzkI`CBl6+F*ep^=Rr@d3Hf}I_254vuHY@sgfCZR6pX5W)ze$12kNg|!T zjniwv*FR#Vy+fYYigKzOWUFZr>dN?Q+C8SN@|}EOsj)G5htfLWm45xoPTtOEBWhU& zD70hr^Y<5?;@q6}ss`<(q^ei&233Kthd%Y1s)by!V#we&@#*AHo{vl)4!P z2eO2_vp_^odp76gNliVX8uz9ubw33-MWy!a#l6E;p`miiHu{VqT<^*vvbC^YM!i#S z6W-_A6;?!nuPY(K!6i;?AmzQf&h2_rX3fyH-_~)HI>fMwR1wR}67hP76XbT*F4Swc zv{EB6%pANqa7~&Y4B49@njim8k;m}Bs1wYJ zjGPxk^>hHu`!p87DrTxYpE_E*A2!;|`P+Xa@Y&~)pc+Yzij|m>ZEhBVl&M&xSZvLo>$T225KENd{5F}lE zw+O!wx~oPrRQ0}5HOq5tGZg7tk33OLk_{&u$Z6ThPhsx;3Y0w`j}$aN;+AZ(1|>< zEcQlhE~1$i?uOP3QaTSLVAJ-(yNsQ7SY{i%DiU2|a(E9e@nM3VXJ>i(c3}$g%%U`w z8crASnm9DS$w^6)VK^|%rGh@Av|~bmfM8`=+R&)w-2LIVeYE@?R~;Uq!dIcA-wc_% z7}c*wAK7qb+`t|^(Ya)@=e@f+%*4DJMVuci_F%&ry_Z8)C{v z@cWVYl@W5z72+QAehz7ud?8@HamHM|H6^IT*>X61B*f$pq=D5dGC$e(a-@|x%RBV? z>N;Y_SX8dkZMA*0cW%s$8-t>oRscQf#RD^=fJsXt1;^@M+C)<^zgea)P46ObpW#5c zP}T`1LPIgMZ3R0t>#-O?~)Z%=P3kzxQlmBU0mYpEX}I~s@1n2AiUNJ zBCfmd#anyqULx+A{S0)D@&C-ke52!gQl5xLh&3OrMr2)wEb6P0xPE|&6=S|b9liiK zvAG}-hv^!Qw?vmVO4%DY2`+bx*spp@pPaqm#)`l-<^HbV)kxjdvL98Mf#I4>%cs(H zbCx)G%q5sH%f#)C5$FQ%d9@5dACuTgsLz{kJ;k_Gl_&|>nQm3ZUsMvEA%(=yQu3B( zOxMJGDREFS39#tU91O!*`Y9GYcJc+1LugYtb`UG>AM}p6T}1>Yz-@FHXbwh3&;}ju z#`2Gq661VXpi|p{HG|&$LJi>Me=Lo!zZ3YYvMYLcGkp7bMfG+_pc&l!*zl6tbCg5@ z#nZ5xAD;Lc)&bY_3ap#g7#qOZN#(A27e|C4_Xc~GG{IUBHJoSPkyDad#ooKfVX8q} z{KBE-;Nxp;x2eoH9AxhUn@lF@NT%K4;rJH zqG(Ag?0W|$)>fTptLt8-^E*d_JsX?Pkz^&OntpmWWqzw`bD-C>wZ_Tb(ejJEw)es! ze&b>glcSY|06917BpG5DbYIfzB(ITdT;6~e$Mu`!L(Bhe8^{28X z4$rbt9`hDCWrXu}!8;>wCzX3D-!;yk>8PP%>A>ek8zFoL!U0I00A; z;Q)ATw|16Y*Jy4)eXt6k3xD8oqqYvq@bygI7BN{SL$kF!eVcX`(eTL+4&PSV$Q{rF zS^(jq>*;Ukq150447EZEzERmlAGwOFtuS>~EoCw!-^n4BY94gU<`z7Twgno%m zuXo)xY7U?QhUiO9&!=0hz98!j?r}O6+cDuA;Gn4NvgkM6m0az$riJhF=JRjxMM8Sc%`NtxNeWP=8r^+WnIj2yxo9!rGBPfpd0;hgnr%* zU;-3BQSy&ZiJ_w|j4;qnn4gg=U(7t)kV+Hti)lgRyXLKUPjK>MhzcIN`YcC-JTlj1 z#U3+f`{#fTvPQoXMM z4B##lx^mVBQA#T3ikXnNctp%*K#+(FQTbSq?PC&guIykrT<6$mfBcr&DOKND6{O{E zwz}sSP^n9j?T$!0tb+}iIcTN|n^{2oVhyS3yT0EwJ1a~NF*%MjG|gegE=Pj4k`7d^ zl&JUdMJU_DV@FNk@}?r6E+|M})AnDz+)n9=p-XDtHGAFc6Y21x6OQW%7^RrLV*#l% zYIwHr!%57I0DFlnehd!cItp_8po;AeEduVHFAMc@Lgh_v;4^8gDK(9-%~%0HfF?M@ixB3mQt-whqTXj50S-5Yt$}ZIxRN7`uAxSn9^K!!tx=NaS>S z(3NdB2HFSh5tnU;2&*X4?tMcW%qqPQ)$k#i=YZ~865UfL)`E)VdbK`vbSo9_l#oE} z>`e~pUe)y)J(xMotfzQBZr%5>?}5Ts?C+l*EGyT|J4dGJ%lG`n1@O`Fq{#f;T zYT!Zpytm_rBdQPUsbuyN!)0f`D^Z}`4Zagamj51Yeo61AJZd=pj4h}?ce~VM5tSFkuDR{B6w>}H{pEeuL|r_ z?g$U?kZi3wP;V2DL&YVVsIYRF`YUwxUSRg64oy87j1R0sPMu#INM+KImc6q%Y6A;s zy{@O;xyp1MKZj9<$KB=!NFR@1TTN64Gzu6{$0XKyy4;}0c2Dp992egk3^aZa#bp!r zq2m#h+Y`ESl>t#j?C^CHgy++HvuQUZ<$YlT6N@u1!XBnz*gnZf3>ci;0#*p;6r5c- zrZi=@`Y-Eg^afpbD?28DVhz9QeWr?bk+*|DSu zNwi^7_x9QypC;ifPz{`#TD{APi*p_&7E?Q)&ZNzzd)U-5N8(|aok5lCIaEc|nW;l= z`X$?%_>tthPjPVzD>i!$A<+brz1#ycgY^NO(wZuX%s`0#Kj}<59kkS=R8gOb;ap9{ zJt!2!jY+{-RDaJ*k{Fe~Gk#1F`@Thop?65pZ!xp z{_eC-LHUC#0wWzbM%wzFn=mO%0}pcYyt-G+1E*_RWm4uj?L}{6SrO!1u9j^zzkc{7 ztTUNMJqp)ydQ~UbeByg^?!1ab`tobo!%eI-_FAE-fJZ9^*7G^WjpHrS#$cUsknm@n zpUxIS9|iXN1tqC>{dgUh1PgT`G0twJ%=vwq43&H2hIMv)z0tD|McRo=L9;ZxG3nkl zD0!~S?ny)2L1Tt#f@fiO?(Uq0y6^1+R}Y<2_B8zCVC&i42;ZjW!w&%Om#t;>U52Ss z@v(S@%SsRn9UJoJj}n8|^qv3%egGtVmT~eUv~pG2I7LbHHwD+ z`CjQ$AHq84cTK0)5y(-UKI{2^t*)VpCe!tAiieWyK$aIM;{#&oB~0l8+@i17bi&9I zKFGX_CvU?~T5SFD9M2Qso@#ce@qKB0Vh_|jO&dx)=@Q)jl8R~NQ3*oED6eQmjnqu^ za59nCvXhdl>$`7soQ?ZvREl@KQ3?VuU zn>eI(Hgux!&D^B{P@tR9rVbd!8}E zK#ZZIYFSpe2IL?;EnZguaQ(_;{jlDh?#{YGZdg*_InO-x1L5sxHgT`n7EqiEF$tv| z{_ma(##nj@R5|FNEQU4g+HyWT(PQsG#Pl*IxLj)4A-(TFCpU2<&KsAdYB^0KNg`f5 zaflRv_x1bYrVcfxgkd%Iz?+6rW2d-Ma5;xrxAFy0K9|CJd#FaOTdb$ve1rznYq5^^n1Mv8+lTmU%Y;PM-xcfV zR4{Wv}2zzpU*h^bIO{z5q+5KZJZ{Cp+!nztNWR4a5?)y-|WbG9fm3XVYtkZ@4s+9XifGyi%WL6s;3n?zuhh z_B|{m_k){lMr8htSip(vub}c8{zKehoxn`->lMCD&^n6oOhg2s4qN!Z5eJg?+l1Uj zU+KuASOe%jg=55#q9L}y&7w=rzkqWNxcZDf(V`J(D&b@Ynl!x()IvNp<660(4dj)gZ_K3{495Q2&eaz* zhSkt|?v?X1jqllt!i5kz1A_ZiYS zZ@#^LmuhuA6^suJ`d!>7i`I#Szk%dM;K104<{87E~#^qclXzOu@B-_OQq$Lj@;7C+$Dt@m(DeV>5{+CH%> z>}{86?`BgN{OwMIZ*_=BO8`w_j0x!&;sn9NR7FBNAizXjMdM`<*cIO!xqYY^yvWJi z`L5*(J|D+iR;2AI8;SIvMM_w2{4(#A8GMF>(GwBG-6wxK(p9stQ0cR`Jxe@f z6_KX97KSeD}u=lxtCM~5Vy58$UFWwqqvT9iM?17M`pAZ99T$QyW|R-)2> za{j!Co_qkispUQ1xQDN^)^5J~#KHWwGM>Gh&C?dWeB8qYh*jMb;e=$nrqaC;KhRVa z6MCsVrpVRGmM7EtHP5_)Eb)uUfQkCVAbt(A7pef<5Zx!*76Ox(t4>Z+8Br^ z#d_P-i)}keT2}M@)!Qjf9aNd(E7lG0D{zlHpY7&EcAK)m{P-!&`A+5eL;R?QXAa&U zqUC7!rfXHF(XaYyaX1gf_6eb&u;PyCPP%JOXeFbo@z02*NhHcZSAE;nq6CT}eHzl>l zN%oNXw)Ym>v0Vdim`7haXlUpIMmH%olw7h>BGc~b}RkK#%HbkSci`E0Xe`h$s>hxiIb8p{b;Bhwv$N1aiM`bsT zKVi`glzA5tdGy>dsZq^Qm7R^8D)H+{!NkFo{z3~AWFY7wAm41sZdW27++W|-`8b0P zjlk;o$0cNCTpYA1I<$COs$cC(8Cg#~+wvUQ4b`r3N!{O_E2*RcQl%9(r!@Hmm)Jhb z{U9rV5LAvhG^`X5^sN5D=25O>Ab<^ZObE)aWi97f}Xsw(uIM0A&s? z&0!m;B80K4*R9SQpL<8%9vquRL99U)=&fBs8hCvPp}wHRiIo`}1-hdY*D3uLdHYX$ zpTKZ+7*e#cyg%MFJU@7_zAsG$0Nex1_rIUGywWhGeL|1r#WYr;Df{rLpnk-%X)$os zTxkB1@0UwlFME$NjpLG&tStqf5Em)p% z5y2VlUBy2fHL9~H2nY#j@9DI0QEkm56F$0ikepZny#I$oGpbgI9U)fnn|8 zR_{oQBW+OibI#hT7qj|}n#9H`U{Qy?8NWkX#~GrXeG0=XgjL&bhn)k3kUIoK>EE91 zB`DHf4KRSsytP>kU(&ozF0osFyBwPg>^kbC(3?v40*#w;y>;Wq=W>O{9ITwHIBZbK zS8AvhZ^=?Gw*rn@6UB(}Bsn5*PoHGY+Z_W?r=sLvM4LUFt@hIopptL(&Qe7k4QV^~iYp%qZN)CwFu{3nh5dADj8RD^uK5viO>a5Mj|)C0 zrpGf4I#!!`)wiDQpf@Xs3|sMSq2y#cQcbCJQ8kWz`H@!=?**>;(6M=%{0-a)J6Qoq z&=V=K0J-4Wr#htLHIYd*?upF{HmWqPc}8nS@52gm+;zTjUQU8Jfz4tMAm8#@2Ts~{3S)=&ZW1~ zZ>p>1ccIT}Zx0AN04MMNRa_W=iXngaX8(FIe^I0v+3Rz1_k_(Z55dx^7_E&+l{0E? zep}_}?>|#P`xh+8H;@gi^r}Y1=dAe@Zckss{@IdRgo9hY)^5zsJ?;93GSHfz(xIjIi3j#@lFLth`ZrkDfQ?eX;i>+%snW;iajRqGOGD$`j7ElG+ecqMm+ABQ^m?QTvf7BW3E*gP zYU?&WnBn~7xS%qfIo;XmSpA%7<~pp*vaN$P#+Q*a+^^yMTzPdZqrnH3jN8@?zIXi0 zC}R>W3OE&eg0HVc6RR;PfU;!*=QA&lr75q+UsV10J6>Tt%2Ba2^_MPV__X5BN!m})sW*a{gnahvns8)~kE>APyga4(})t7sPVlkHf!izMOK#LsNyTiDX> zG=k7$51?(<2<$`cHV zj2omPLk>Xs&CwV0Rcy)9L(u52zLnBCBgNylD z^>0;gt+D4j5-ni$JEs6VI6;@?GG1(X+iE@<04#!4cfin_)NyLO*xe&2)TpjV8fedwFGm!-t~>kjqvJ8tYo0~poaa8RuUoEDQ^ObT z1mwOhw`k}OMTF^1KMea&T|)Qn`xSyrIPKX56lmHwpPwA(?Oljgz8~ zXp3&c5Q1TIfq;*k0=*OwBFjO)o*oL%<#02tdQzu}{}1LaHeky2;>W+|Ffq# zo|zPu_dgUB+{WWx3#or@XPmifwmju zeH+Z*)RO*t$9_g3em$3)uS4s3JS5xG5^w=@1)(^0KKf0O;=dPDGKcxs`Txh*dxtf( zbnU|w6%Z+k0!kMI5vBKzh=Nj75RhJ^NbfxXM4EIFks9f}_uiB$gx-_TLk~TW5coEF z&hwnN{GRvw1Fn5tN%o#Svu0+^TK9dgPHv~eH(Jku3FV?6c0s`zd;J#k;|w6Q0RULv zWICDSBxgmrUt-`502;l@QvTsTET(@x+P&w100k_ylk!9Zc=s2-`9?ozc+~$_bnri^ zecfdK*IAz`23#Ry1C%`Nljq38XJ0M}{htqXWW_5|TkKMMsgt_C&Ood1>E=9nc0(=U?u#+%#Ic%F6S9lH@`}{6ncYo&z|AESsrv4VGK>waWin zX86DGSD4)|g5hjpVsC+cK%Ny|C*NK0U%v3q5&ipmLB(iqspoZp=A$M;Q$#bU(*L@B z>;I2ui38vpJ^;CBOUMdz<^9%>NG8+Re~$hCcD-~Rf3sClNeQ4|0+3o@hl&yv_zzp! zAEtx9VdC4dMm=4Af2C-MJ$YsyGGLCM1?h&7k0GAdhcH%8H7Vvq^DWT2~{M9IKrzvL!K-vGw+>$G|y7DdNtUl*w?| zB}L{IVjs*J&E7yE+Qkh$3u-W!|7_?f0GT8SheWdjwcla^mQj2h(M=brXGtly$Bn9AZG z(TO>g*2&iUuhKu>K`J$dqWBGu(1cBogqyV6=Qa$0Tt-ZmM_v!d){g_K8r*G(#Zi{dpKq{mUzGcdba# zgKl2}(w6gA^{y=n7U9BjGpF&eIV_VKyHvo@2p>-)o2emvNtv&iStJD{&jw53>(KaA^?cWJ>$L^U2fK^2&=D>Z%-GS_n;+v z)JgV?S<&#SyHv-&rs!R&zg)vycUjT==sja|Qr`7ZrMMeE!zB0ws_o+eCX5(Tv^99K zwhYRN)6-L-Ct&Z4ZD(&iK0)4dl1HW{_QKp@+*{KIp93f_(~3pY-(b9H^ijCO(APG` zEnXj~tQ`TGzb&@w+ZFt@e4sQStLc-y9%d;%%WX@;xfc=eWbT43Gk_Gaekjj$$0hpt zD~Iw&mW?020RGO0k@LpqiA)6HO7GRp0MzH&CWo3=kMj-B_K!V#2NlqbM{sDkkDjXN zylBb7FiPe*b26Ukd^=O7Zj`Q20vJ$?jW=|APn;HqB-S6L1z2hGYbvoWffu*%|JaX0!g97@m9ynfw&b@Gjb_!4}wF2e;i-Xp*$wI7cs)1I-YKj?9pljkz+(4oKlqocIDt$6Xa zy+LxJH-3&8hRTys^*KkAe55k^ZaLYMA(-p6tt^b90QLjwtGwh+^8DT^%-t1nx&Ini z6ILU)3r@8rAj0_!KZt1nNFq7I5We<+m{WjE_QI@;zxvxtHaXkCJkI_k zk+}0b+am+7xeNV@HT87!I@h$=LMwWsz?MHDMvp|gY9+%cx+6KwujKUitYZothv8~E0uZwivfXLj6 zPY*_*<+8RkT}-o_91u2|YRl~(L#ae(Kx*R|<6E_jo}X^}ZwfRw-hcn+e=dzZeet&3B_Y|fb8VW|v9_`0bdT=jv3`ZYTJ+gYhMJ2;t`_eD z_fjv|q{hx`mP2gso4uy;Ha~qC>+!-kuLsh#Le^fXs88`z(*(TR+!1}yu=wth-lJzz2 z1fHEbFltYUZD)~)KkrWza>}JGEr9_^(S)7#cUy6ZiFa*znkLDqKl<{cocmomIwM{; zYCD2t(iwp6ds4tjTqm0X*lptxMt2X3D!3W7cY(uz#jo#&hi~mI)Y@PvYB}hHC4UMS zJp9ERmnz|&dYZdk7FTc<-Q<1rynpFVJ!D>*Tl1lJ^3E^O_*B|3ahJ{PlFs$+33KJo zS8w%7YrBOr=Gj7r;0TW-$ydM&Ya$kePUFm{XJMD@_RRH|q`&x>C5`GbPRw3Ndp z431)NKmFcoxjY&hkM*A6{5Pa(X)2AqOe!<-DH9iDqP@pL*XT!??_}g_%fTw1(QGlT zL9k-C-uZ;PDnHh5!P%uf)Wp4u@iItgb}Ht-=AFH+`!mj;r>3KV?;<}Ifqc)9)qHJ& zTXd)KNg_wefy@Qp6c~ueYju*;21C+XFwpj91$5B$r@$pw?l$jONYATp)jusmrRMcs%sdU!BBHAihUcM{~Dd70Gr3&5_T zIN-L3Z<9_1IAgd0d=SdRJpeGfGWz06Lk=oq5+R;ZZQcmvdJ}PRU&O=)QYsP#5-*t% z`D$^$*ScZ0(S2?)-|Nt7nmirX&+Cu zmO@wo*4l(A?{?q?DwxI@NSpUjAD)r8_m9PI7@l^x6E(}U*9C|%)}xTj(0m71$KbjV z3Vwsh_l3GO1~OV-AD$>QWt2p-9TsWTsM^8|de)_8`hM;`RPdsuNcFS}-<)Ys+c5}4 zOH7KkeLIX;-$)ugb}cM-4{_%zaaV9pFlj>U4g~o)sx2guyJ;HP@79_|_3TP9*&6bj z^>1N|q_U4z!5Q@}=i`V}?VFW$IIpIp+X#P`zkJn~+JJ^eOk(2tFX$`3`H|rvV%{AS zDa)OgDWBsV4I6JBHf%^wl^SPQ*nRW!-5erv@@br;%8rsb`u;2VB-`gq;ixMu#;`-C%lptL{eOw0LiT!bBhs1(P$S)f{Z6xlWz@EW_=l zjju2CB}YwC$awT6FqbXPdn6_%g8(4dG#yo0^-FxJPI;yQP4A^QuJy5ofPX!se8reAP zgq0Jizbyk~z8(Sms3fYNsr0tfOM0G5^T{C3#q55oYam6sbXV?5^4436G}4wZZi7wO z+QMuzjj>B*a-xwJp#SXjBrqj2-oQsyWpGm?2<|P>en;Z&M^m*359NG~;&}Y#raxjQPX4g9;Xhj8t5B$NC#U~UCqmCW?L#-&n<74g^y)>gB=}@Qyse}MA4HL zr@M3|;>e#M^Y{^zXw9<7n(^nB7B5X~(*_G8d~9c6WqGg$MR)KWB^tuFH1v$In=fd_ zwEJ(WnSPs_bgc-pteectrs(!!G0Y3w?99S_&9%cwBpDWEW4f~~of5xA*C(K%)qcoR zgJRf54*_K14U6FG>GkD9IW_QM_2Q~J5b=Zw+9cs0EW}u;Ri66SHzrG zFwF(+(#)wi^Rj@MkQyoJB)%)Z2Bd2gCY{v04u8^{Heo*m2r;f4y$;p>t%sH-lQRSx;d9z~IEq z17BeN4(AL+8+Sm)-+)afU$%WxT3i8P>JFL~4V?2inAQv4q|Wg3Cy&_>D=R5|)iUsc zdn$c^;68_t@J~*+E?suhGrITr)y#+5Z-PJUh($Bs)Yvgp(V4`VLI}?Sh~OET_=bM+ zUB=IzKn-dFeZDWS-fp-Z-TZduF%4>$s5M296x40%6gm2`2n~yWHhodG; zOTvhtl~*oh$9?qGvyaM?_4}PCo9LBpKp&jVw?O}rw%Mg%Vc_5}%lSOS6;L%;GsP~$ z2Pk&Sl3HQK$KAA&tBjx`9M4yA6u?~pVGk+ZGgWT9TBnH3aziBXf|Vn0^+6>io_4A! zI)5Tm)gEhf8q1F+ace1;ps6Q+DkC#66}a1qL+7VRARA2HK^_;yynXbE0bj^UV*&rP^hjY8ppX3eLA5QA8^EQWUD-^2zKI?3F-nrfN`I5G&fXdEDiX-730CdA^tW@9P%Ub_L&e9}j;4Ry4qr9e< z7mPoUarKui%(?+=frr7Atc;smlAESjgCDh@^bH*-#Z8}uNQFkGXlq{&cD#eTDzU^r z%6-GRWl?;y{4t_Tzi=6ZIU)_tIFF(%$TaVBPtzP?eDC2o)(v9uCz8P`Ta#+;qC0fs^<}yk7E%An^XAz7A zfdV@e7&xD6uSb(5(S$7T_#2YyEi`+jy4CYf;F8j=lvF7@4N~sn4ztMp#q}qm4wPYd ztm9?38-50pF|FMsS?mhsU@UQCv2|#cA4LCZP^84avfbm2p%{M;F8a6~G~#K+!KQcJ zth>w*p?f!fONw?eZ30Js@HUH!W*(x2k?SI5v*i(Q6PqSQWcQ76UmJP4(?xrfdqZ;S zakzGBmf{GwGr?Z>c#)0}k$RtS-C`C19rhkFl8-JCl4hZOIDv&>FfU z#@u@B)b<)Da^7(7Ee17n@2#Ovh3G~_3C&uIS__8L8XhlW-k)?XKi&o5`Gr}nxcrt0 zjL{zt-f!;r^4->YTOpGO5yE!xl$(Qg>ONcFBzJc|Z+lCoW?y)IHeLR+=Ji!8O=yyx z^Fp!CVK9|Da^P2C7%|=bI{sTl(S_Wx`>LY7+0qY^MH$51T-$%O9o)7TolmjbjLRC8OJK%fF?1-=d8E+$ zxGeSu^6gvotmjZ^2V{#uTqsIllb zVVn@hwFyIq`K-6=th{UkM9wQgKVIqj$+s`U$o1>t$yw4X@_WQy<(5MxIfX{3 zLrvEUt3`sJyorruaohw4hm?(<#4opquVolc+6EnLfwSB^TKgQ2sw{bgdNw}J%jRWX zt>bZE9ObOCRIS6zf@_dw&wqZU%6SHrR;ABs?%O5OnzdgioFdW6mo6VweZVpBP$vZ$c60eP~FYi?k?p zeBPwgjf=8~j~6%zUcA-kJO)D!wrnEvwG!G-E_0%4;|*MES zlwc-Md)?U?5hF<*Je(yPWd6%XU`by+&;D0^C2VZqa2u(mXd=x)+oFO|g!WD0y20@- z=(R>mgf=A2;E)geyeUE*&5UC3uqaQ#HGgz#^$vIRQ1bW=eq4R)A)IvP?|u!dO9W~IL!ULqK*Py7)1{=n57h+Dm?7H9oTjzAGb<6t zb+;)@JvA@q89rRR+#GX_=ybuG05@0*1dM|lCvtx(vMZiX+XilAfWFpu5gzC^Kr>Kt zxS224Z6l?v8njXFnHDV#oN~6^XuM{XT}JEPouiqGt-t-V2mtd@QJt%V1>zbTEhM4j9%azC zDDq5HDy`uAdD9;K z)oJc~F#XqWvjNOMx4tltaFBmgF>rJs!z8Y$Z#%MU9&PhaA-5@PYaDJm4$&x2@hUC8 zAVQa|6{DLi-E3$MXPofoSclpNodZ;f2aurA!8>xMoT>jwjwd~5& zr303v*iG&fAp9VJewWk(B^H~7k`V8b>F(4ZSo^CKN2nhg4Nr;+T)7+ZD#K6^&eC!p z(nI)o+)^LNX?hMdmr2XB?zR#zcK|IYMM!)}e9j>z>D})w42FftDL*l5=v87uFjZHe z)O2~@b+tL#0WjLge#)n`!eG){t-pS>*~?uwS~8amKYY7vr-k+`T)YggQr0~N85ZiI zQ6HGXwT|IJ+~XE`*xTrS5kx|!JeXxwXxYX0#Sbq4D**l+EBukhV1QcB>k*!y*}gYZ zXcx8!ghIe;d_%tX6#P2)E9vOp{uD^4sL9|2HyTjQeLAH^_?KzHWHzL{JLwT)(<>BA zA=*PW?Bc&(gnMVzoy@g$HBWr>>zOflZ=z;5#S;pqDz0{zC#)iv?vn!24+CL|xS=o@ zdG;oF;;xi(cLD`(qwLhcnM1YKwP{CX#^d87C+#(p1*7y~H-HY+G>nKOYCgKkQ#dQk zow_x5w#x5$2?sDl=DV()Ofm5t<5lSwx;`}&w*An|IBGl*QAdvBPOgwqwQ19-wWiDt zr;zZRq`JH!E}-<$sn-2kf+)D7H_M7k{94$Zqll(UIL4$`Kd>B#FqHFXX^8vw4 z=j|ToeOlRP9(CQ)a3+DTTZ+@Ni-be6e6cQSi^A5unZ5W?`2NN|tsz-h9h0vW_gG8W zSDEo@k>x!sF$HH~S18Ev+H~mya|d&l=SmW?-GDU4%E@AEbic=_Ui|xi<{NG{S~9&&OmHu8BMTJe2C+2 zEkXtI3{r*CiVZ&bDI(UXSK_YSF@%C~GNNjZnxc?58U8{KoW>K+;XHr@t&X2Rj0WL* z4x*0mz^)Qi*cu%_acoSBBJB*BscQPMt6rEjlrZ(Q1S*DA1~kyas8*zp?b^_L$E7v_ z-n>pk@-+_CsYBja1dFYX;Zn241>a8cRIyraVJPEiHxRA;!bWSHoHsfcg;EN(qX0JNxbHRn?}KUf$wN)ot~ks1%y?J&=@WrA*-yx7V z{TMoN^r$1MdE+Qr+VsXtk^FUu{)6|6oycy}Udx>LfK;7T$)JXLjCG1Mb(DBR1jl0w zNgJ2sp;f`WP5#F=@nlb#$H+;x#sJBJ_dakjH;3-sf?it81wj~G0Z-+Qd%!?_W7|EQ z{g#4?=7y0wjSbsD>@Y)@Z8bkhR!=eWx^==|SVU`PysxVpet*RTZgWJdn z!&h!wAfmRLEw>3*b0fyD0284NPP7T8C2IN^&)ESkjSuh^kb%|4vlH9I(M}uKkeEZjZl zik4&PF{c3WcqL^2gvRhWWTqT@H+)C<6}rmt_q35IdPSxGWj{&iYZ?U^|Ie$Z)(9H# zqN!vj+vMjDscr{xG=?6XH9oaD{j}acm$#N8o2Q^u4ax5Jo7S}M!*B-VP)f-cbLOg$ zOxvrgv4rP;O|Cji+e=f+f){KdkLpz0nk~v$axjzUrc&29bt>B2CiM3#tJs*Aj;uP} zK5!^zU;CnxEK^112b5>)`+TqOk{dyI5_U-9b-e~B$f{F#=fFFPf5H<7oSKY}CRZb8 zs&PUXPb9dyZqYMkCY)W(OKHZ`v8w_b10f3s+#e4~d6=ssq+|SL*w-o3r?qHucbtvg zd#bJr#qC)o3FLD{HnoUfU?lDN-u{K4;u^0^@jxIExsxg3#R&;_8eUOu8>4Wi#AxKI zFnaF8WUJ`QM>whtAZ^|qhY7w1`{H2B4 z>g(^f(sv{4Z!Lgn|Fq5CtOvRp_u`6{OdjJ0sTC02GMWo;#*C7Pu^SrKZhscm!enlv z$mF_RC(I2kTG;A2lvS<{UFF>jHSy+e&!ky$2k5xZKk#poSV$*#sLVcPZ7tam@`ktr7tJHNFyYQ=DSZ_ZTQX0$@BJ1-}-kb`0`gF%vXY5w-!E~TvByxGA7TY<)P z#e%R3GT#}3aE}A2rL@sUp*j&Z<<4N1A0J*8|GeCvOkqQn>mt_YhYk8R(%P~J#O!WM zNeeriHlS{^J-7W9Nntv}t4Stu^mCUVaAgk4;@d1PJv3~7Y!1+brO%GlyYA;Tw;n>J zdZ@O+*PNIpuM{=7Zn5dx{YqVpBogqU%pY?~cY9Z7#X`^#83C2ht)aQbwo(YzK1^y)dM}$|r5UYr2XNjV6zL3vHR! zSZ7z}UFo-*d3P82*b{oRV$GusX*HR4msrKEqMh!hKl1B~01nMat7vJO%x!kn=%XqQ zrOi-_*r1cZ7y|(kds5!LjDa$^MK-wsuoWRP>t0F4BDA2g;cFh_!Oj4IHqmjAtDo7F zxVQmOe!(QrylZqtnyUTiy4%_v<|xPred!Kr8o0)rnq*XDK9sHUWdKB(BdJJN8-oV76uoKgnO6RfXvzhw z7_Fe4@7rqsy$`?>`aNhjDteR2yKUf1%$2;H}L&ZiHm{GE&o z+@=u*dej>gf~x{{Z6BC2y`j3UXF%)6u^=3VY#1}ViL$}Ya1!|uGsdJA4OPY>u(pHV zGLK)~{u?5H`KzDp4>}a(O`~JMEWoxFt2kc+kYJ8%e);{D7DC*tnxeRZYTOVFkCmsceafxi~>_K|swwR54Y1YT5zetM3 z-hz(l+Cs34yt6pkCFvb1qaU$TH2W<#e29y5s-`viP6A60&)RUyjm}V~)4lb%#0|UA z59x|rpZbZ6GMn08SISP#w*7FL>aswx*ZwWXb<_b}lA?LRwt*DLVUqRXid{jvwMS&)N(&sV z-BIFQUXZ}Wo7N{W!fuaYJ}+dDO@jjzi*t@$438#--glaN^UqXXr?#AXOIt+BAH>|> z{-8AkDIiGi-N=(s(s~($D#d*`YuL8D#p}C->_3*{tyVRa4*dCR0UIao9D{|L5r?)?W)+_ zALk7ApZj-{66#(I*!rX$)wm0GVIGoU!uNqr^lF&_RsY2tcOI1r34Tx1Vo^X6wTu&9 zZDPu74441woOe0qLf)EYM;s#Lwj9zYUCVJ)<X5iV}AFVjBB`zUy4T` z$o!9jj7^4%5(f%wD6V;Gz`d~g3(wa+aBxL1DO)FmCP~YTklx>D+Mu1|+E`S!^xwfDE8MajK)#3*Yw7s4H#ODKA zTE?Q~oZ1{es1D+cE!;)4mQ3C(?EP$r^r^RhS1EmG>63a@OK{k2Q)PI}XnoxgeL z=OV}KLVN;E677SpY&-R^v$&r;Vse^sKWgxHZ$P`P{Jd)P5!8ZBN|QP*4Om_;NEdO; z$5NIQY&VQCY5_X8X$w%9^Fjdq zB=ev%iV4Cmrf{m%Xh9cdWHCKeEV>$P+W5{gyDnw!AzV%d)*!+F8Z&R_UqB%^;RmCo1fEQW2mzb zPvG_{BeK^=J`g4<_7`&58RGz&KLr*Pl-G709%GXRkisP(59YYRR`5k!(W6c`k*OXe4Yu zt=$*;HpdIczKTs3cRmaqPe^2dOJ=8l?7w1!N6_&n*MH6&*@Cd1GsPv0@!=Ye7G2pV zm*e^%m4db5b@N3Ip^p#85P@ar5)PdfnW=Y$==4Ad6 z>|seMq98QcxsCrPU2$<^eWA=N%SN^~cCC2#YsxAb_7!AhP-BFJfpNyc$4BRrWe6{B zky#I21<#XD_jU7fcMLNg>eY59aSTD_?EPic?oJrjhhVP>dFB&X=Yj{9EOw4|fkK4g z^|FQg_hX#Jf9)2bP%qC1FbKl74e}AmEMLtT`$X4+*#mE81EeAPnllJlpA-DWW8?S? z%LH2unRr`a?!BK3+E%zel27LVxn6~{aqF`-S@KAc;dCx%+b!-#8c(9~J%Yp^{Xzzw zH>IP4cI#{4V5me6+-pPMe!LWs73#yd+Ib2oU^h&y0i8>a9!awfIgig+eqpSM)Se&+ zWz6xIs40_H&$t=<>W^7xX0{4ICDnI<3X7WIbRxwIK&9q9N&?!UC}&svYx>UvsJ988 z`u@;w^l<#q>!s(DAcBL+6r6ouHy@|F(IhIUrds#vNGK-k2QPlOeG9gY3)16CS!R3Q z3h+f9r23v^UEM$UQpd-GR?Amh>YTzc(+m3nc%nNE?aC?Quo;?jigocOmdF6EXRak@ zctM`&eCwN`2D4qHMjv4oWYY;NHy#*(%``^&*A<(T+>kBr?{p00$n@4Of{tT2en=XK zSETM|zt8`q5OxnVKRrYsDf!bX8&>`kcqQtW5J4^JThyNsiQC@~y-?EGv_2QWdM-1@6VKQNJD zu8jAbRD;EuU3VGo9DY~0oj0+uEw$XT9dft`z+}!g`%57HW#VLW4TtMOU(RyZ#G5po zMojcY*5z;8r6ykCZ3hcC?5ow3pt3Aewxb%j2T!c%S_jQ=!b8=!?2`rZq0o*}#GBpT zl96^FgVm~%hNlx+=SUBS1`K7?{vOez4sbmCp67S7&|9s!VQ6pYi;{z*N8_#)(&3pC zNf_|(NkB?Ps4MqLl)7z+y(uXBN?$uK!EX!9yZ?Q{hds20%m@IqOPl`>>y zv|8?UeWitYBJ7qg@_eXYP~X(5{>ur+3>X*lQ(t-g=b-(fFM363=`No#p1errvJ;K5 zSM1Fq0TnaP{Y$=*E+X7yuL$M)^2|u2VYxIFXF1;vyDs@oP8dGY{zB3fP zI9QN2z+>H%-srF8vZxI629?hk680Ew%*Dt>dH1dF?x{IrH%cPDBYi79XB&47U=;kt zg_!&*w{%xE>BDfXVoNnXD+gtJjoEsu8?8GsDfuYQ^C)#poAvbO;dsFYnuxcu(D;{f zx`I8-Do@Siip0oDiHO>c`HP&xy2F91MMLQu`C$VfZY$3PSDk&R=SJfY_;heC^IY`& zw8fBB^Y*y3iJ8Cu6L^?0*khzWkkfb4v}DHJUj{U0jbo+9)TwYDoe@xZV&uZpY$fJMzNI<(CUO^;K))`#d>c0 z7X%aDP|k8d3Yh5Fnt1(W?L2bFmwa7NN-H2`{Ie_4bw@%VW`kEBf~6+|*Icf-WjlF( z(0GM7!?kQ69s|>CEk86m19=;9#7s{R(wO|Jdv{O zTcSd;Pbt{ zvedRRKn{?}XYddsaV`~b#!k7JFA;y*l~?_H3B;M@MOk~CW7htZw={Y~vg%sX)JECt#(NW#~w-WsnvR)T&h$#Bic5cs&>D-6&}>%pB9K&FR-_v+pG3l ziCWnhtR9@ub0*_!yUC|U)w_{_aEY~jtf3-|tba0XP+UMyYnRlpT^>vCGI%zeChr}4 z|Ja@L5lF6*!DOpaWikdz5o_Mh58D*3a|SUH*_)Ca)Vh6g9mk;&y23(r)Z+%$kA^<_ zOecusZ|qSTNM(?@cm$aKkl?ugEDY>0qJd|5x223P~qQ-ZE7S$VY!1`D>a@!>Ow6acbm`V_MWwg!9ll@)_u6nfP#X!ArJasTdznD23-yW22=&jN+G{?dqrAKyB zLk3iyJVCZeGBzjH-kY#9jB}Xf%vwW2BX5zWp?wE?c4MhC(YkLca=VHo)BOt*G5KFmArs3-sRJ z016#8o}uUVp=5FGstg&$(~yq&CU=91PP@+EQ&d1}K)b}aw?r(#4en}aKgN-WYOt-D zdOlra&!L{98hahuY*dofcw0G2CbT9}L?x}eHJHe|BH49?v%KmA%s;WU*OLPYGuWtu zI!K|Q(iSZQ@-RvVH+$$_RxIJdh$G(ypWTCRdgt9YnSBy?vnCeqp8f|Ll zF-Oy#f=R2(B~F2x<69nUj%{^zgR>MM9%Qn248suzue(6_ox`A64`)M()#4H-XXgun z`kg)FI?u%>;ZdaVH^4<&cK^Li9ggmDq}uIgtzr&*#15hfRp71MfN+?GC%TE^-LG#o zuY>cNa~u0I74<|Q-w z141chbA;}F)iZWkO$J&c&Pp_+wxdfJ;c<(6t|uV(mj;Vg7Er3Mb4n|4c}i!aRr9G`wkWD>x@aI!z>F|4gk?ppBVdtQ!AJbr0h6wcdg zdo+iQjlEIa;&8-!r+SVnGHjPG$Auzcf-2ge0Z$rF5k)(noi_nyB5)7a2s>Ha2JzW( z!$fLB&lbpj54vzzuwe@*2Jy5=_R`U84$&?8o~KxES$+PYJ3-Z$c2J`0yHnTkrB86o z=ot|qtvG*p!uzW)f%@zoC?f9jY8>v4&ARh`)#tBY&7UQ$TzZF=qBt-gnwx{+><)vh zXVhcyg5ZAdDWAvw=-hBa*jmv2B2BffSJuXA&!{Xl(H;fYlrEQSmAPZ-mF-ZSzL;Iz zxX|+15ZaPZAGJD#4lw&#oyqi8B(Yz;E7tH3mkr6$O`Hqb035PHCjD8j8;AK@Xsf%( zWUd!?OG3ZY)H5l~x-DGhbxFm{BI`XMb-0j2oteE2i~)GrwQhM_!n9~Q($FHMs);EL zr_|_bk`4**DjGx$=-!1Vh3W;b3AbP3b@5a=D^T*orJUFEj(dLT7SpG>*dcR$A^95< zOr8`ndr$1EJti3J*wr^QiW}Fjh|nIsDaqMxY>Fyuazj2C>8DzBl8c(`2@+zd$W!3V zCTY5JZ`Y0PDt1b8MDpbb?lcwNL21`?Ho?V>;&h+&gfP4r!3$zHkjDMmzD|*_`YF}N z=R3%gu{^4-*^GcSh9l=6AMG&n^zbkiZ zR{7=m8^?Me6M`cJ1Ox){o`$nJZMlLPa}qnl8956|s&&G}6u^RNvMJ!F?)F|gN`*he z;wKZSF&$Oj!&Ci*G$DWle7I*jF86^90hT8+T`DGXW02#?0Vn0w6)C3{PM(v5w;i>Wo_D4=#gYX&rKrXplN#DP>! zntcVGiB%IH7&Q@1@AWL`v&s<}&aS(JHMGBZngY+=D3e~ABfKXi1(V6^1oXB?FJob@ z1)y@i@#hETkA1%js))a-a~yHbbuS6CnNvXIwB^v^L~KP8zW4bP75XfU+o~a=FiR=t zr=y1ljkyh#LTWD<4-6v`w7a;%r#@UJT@Iu%sID^o0KfB14Et-uCISLfqSDgCTHWxO zne$H#xYIb81MYCkNr1|O_(y#Wx^#0G~)8$b$gr6thQv2MFyJI9fz6dK82CO z?qd_Xv04_R)5h6#Oby}VDD zbjnp8KsQyf``3ot|-5aNj`sfsjl<@URNY8NKt>(Kaq{S~9n z>Z=B(V$J07yIam9Rc@NN({bqaty3Y-8)7*d>&HPSlSw6!P{E>B4Es29{(6%Cf6(1BspJz?2yNz$?z8+-0|9LDi%E4>Xx z3|*0*2WVD)Z+A1z&Es7{jlQ@8R=W*?37pO;B~?oi)@KLEJkuJlV)7Uf z`D5$GT5FF=gyfZ-p3M9RfHzK5lA30#y~EjpH71;2)WC)WtFmRHe{2ZO#Y`2xEwXO^ zq3f=a^FEeqq(36_Oi@cu!;;|LiI9$hHujEfA)`6D`q?zS_c`|}h4y+0@!*_$Ab z8Ipa5JN8X>i7j8}lS~ahS?=}pQ*FFyNy<@9Fi-rfZ!m zh<@Mp&%NL9%q2z-e^saqx{n|J##i}1{-0RHpOBc~WB=56z4u9%_iJLAxOiT(K9p8? z{HIkHKWEOnq(}7{NMo=QJ$=m}b~Wsu4<~!@eX9>uru8n}QsoQm94>K35UColEl4N- z?;9B1x@w8nA;hx0*ZZ2#^cEwfaKt}v5uq*{YbS$q302uifkLj0ef&ZZV(=fnt1U!ur*(BSFSf`(CMHG>&vcCbO7$Nv z_@A%#r+=A&#RB|Wn4v3@A?y2hS;0IJA>LE*zgCgoOUZx#CmTR&N%i_p>-`50?uNPt zY*fhoht`B}&N&&qGc!{H;_fAdix&Umhfi(|vS%JmIva8tvapB5u%CFIT;ffC&UfQ~bo;;i^p1uRmccD{Ov_Mz zKz5b83{I{kt56%%l zCHKRDkq%?uJv*$~y~KOxl(_Uibyx1)m#<%&SQ{-Nk$pgsVG<*a>n4}}`M&?{8G^Zf z?7NJ7fYKc`^>aJBVnE}Avej~swYBr#ulql)nDc+9PoB!j`2(1OWW1*u_nzMUuig63 z$H)d;t!2-wyb|R0-ptI5z0N(5VW-8aMRRIU6$|ra$T^2OczV`}6fQQnteM2mQ~ZbK zGDnB_b<#h7@d6MMQw4;XDb%AT1|Wh(w3S~J?)xbk1W1oXib zUcpfJr%$g4@qRE8Kt3XiG!YHFJY8*>+V0&|%<^|9s;K#GoGeMN=s`HpyBP<6+lZ_- ze*eXK37qLJmjl%@325bB+If7qn)?6nn>;Su>?7MPfdH?iLX`c)^bw!Qdl?fPGh z@#iJDb?X+t&#?pN!-thZyny5gvC$>Dx_OdMnMb@Lig_TPDT%)(qoo&t9+U&khr~$M zMKNXlYdvo#w#1W9ihjV&4LDy9S^Tam#L}hSzt1W8i7xY|bnWJyzdB4%3BMQ_i!F5q zrOtLm4gc{ffcG=LTuH)7c444lR|FLo7$}#6IPg9%1k5sL9pjs${~DO@JtPiNklEnm zjr}EQ@RvWE4DfQqTo?8Y97~p$mCViA@xz(#i%81Fj54lV;jD_{@eclLa{s?OFrHA} z+9>*m#HVcdjt^$Hi{Ip!*M1#_hVcLe(&42o$S|l(tg(EsN5=NrxgSu|-MiyJ&<@`& z`VM+G&E;Psp>jE0ABvqAYV>rDy;-XGNy7cB6dL~Q>eZ{Kv1|6S{!Gp128?tErLc6y z_?hk67t=MpwyifxIsdiP`FFB?S9w~;o_Qdig7@@GMuvzaPxDUWtOra8K3dX@Ytz)) zeR}NmRQf!5)&*Au|5nn1W9CVve8i4%8e6ku5qiiChnHk zk&>8i+7-ObjF+WP=Rj*iI6Zq(NE8D{%5?Qk(3;%8Y#kV59m0&<+-~;W@#mM9T8Wbh zhwMB6lZbw88IAY!Duc4y<-gZ7UV@=wWI;gP$#}EV>ijbe5xS8LZ9lxH3<2f;+S8dY zUu>s!b#>KQtOT_F0e7~7g7Kc>aRy!?G0Gfld#ClUX~#;rabxbDm6cJ=5k94miisfG=ypd($4oAVX!MjjHHU^pdHLb0 zwYM?M&^=~N$9?M!%djH1IopVSKb-!?jT?_~EpI8%f!ZViasU#R*QwNn;m|vY9KY8K zzIW`ry1iX-?1gHuBeK2eU2e2K<=#m!<<8U(>7irdZ`IzhS2NOb=DvJ0%~HIuur-j9 zEgW8cyfjjp4mt-)28YIc*-3Nn+g^b;@LR5E*2LK_3&lWZA#^Ic_1In4HL3@KG@#4n zbQM)*BV3MvSX#SV{@`dobbZ}Amd7f!&M4-V)r-7}(S;KGX!ep_5)G+3s-0+qemH-0 zh52yE@!>(K%i2h0m;uUdrg+BXD^(0=drm`&> zw>J{I(sL}zAv@XRL&dni;_jj8a@)aDU^s2II(=tWIV?2T(KTYccSi-lZ#UOLdf!7# z+=!3R{MkT!3(M97`zUBZ?KrGJg#7vyF0+@{Ids^Ws2%c*#otXW7Y|Qb?`t`zI+7P; zNFCYj?PKfy%Z)h;jqktCotb$gSKhoa2M>o?zLlGF(hK09 znCd@#M`gWTHIb{y(ZI8ge)W>o@br zIl6;f+BF8nj=LgU?WYE>4lRD$r+VIV^~ipl-+Doo>3MVWy`<n%Ar2zd37rLR6)VP`Nb`5pC%PQmoIHruax8b*Zf{NHWi9I`zd`N3R=zykD7ku{ zJ*okY`tZmiTFW7vE=W+_X{)yFDg+-l_yyI8aZ7H$aE4}|#-sR6{&@T0D>^5lPdfsi zdd@I#kG!_H2lhq}U#zTyV7KX&<*Gv^#k0@1?QJ$;1vE$80&m!%A z9#E0C3(P01LUIE`E1xAJ4AINmCo80BM<=RF945yevnVdXkO)qUZi_WaVpG@noURuf z8LBrK79UR`RF~0NdpLKMQ0!;J@tRSe zb@|}af=-?AgZ8H2cGtOS*5%FWLofM;BHk=7Pdp)s2r*PDDtdC;X9;2+gtf=dka6=v zUCXK}{RYva={yMUzg|04@_s^Q7M7d&N0&gJ*}T|?oXk@%>6a#`An7%|Krh*_*QgoM z_eroX5qno@+zWCNi;yd~J|kO>hJ+E8d^d!RO@nc~BB?Dc=%iZLg_hoigO@l+Q#l%Y z>_mi5UaH+XlHQ(UB5Ulq{C!gbUub-2#dt8MoWicFn2?uqyrsNC66aC=VPJ3CEO;%Q zRhqfoy2AsZ#!-{VZPcBKv0TTL@*A6T7UslgSDt)1Do zt8W0;!oFw>yEQgLt)cwT>Cj0pNyD7D=xhBIC{%Tkx5z$vv-*LY$kzvBI{gNlw-YDq z$3L>?x*x!`C!97Q>r1{wmT}v-lt+2PT?4!Gi3~|Do1qB>*qyvqRYPQA(=ln+R#4e% z-)5Sb##tRMS5Bo*QM%S`eJ?+Nt-2Z~z3dMKR0o(Wb-cQBg58qc(TC{G61YT_`W3-o z!Ihe+qYRc&$MtV?sCD}|l?O&T!Qt%vLS!Y zS|V3xQL=84%5lkRWD@O~=FX>NpJntKKJ0S1{a9xU%$Idve@4E>Ks^TPg{CbXCo2NSRy0-3 zog=2CF+S({gZ;u<(FNYvThkmGr4qTmHR>dSh3-|$6;rn{M2^z6B%e74i-`q&G{r)v#I>tX`iZOwJlOy0tJHq|B?bs82WDf~y{E2O@B+_TjV zQ*QS~`I@{C8KA|2n#4oe2ENX3dvZd%cYBk4?e{*1h>E9AG+{1XRlw0`t@81IQ4Y~G zx2S7rSSZmJ%w2x5gGrtzu)o=1SZ<5qeq&h|>1#1lQk40FdePy-wboVZ8PM|9Vi%7A zv8bNq<49^LGkG-YQMY4TI;M(3Mo58s8q+`&#Zk!;1no))nvD?g5>_I*-}sN8(SR_4 z1$bqQ5g%{uQ;e9*Yw2%+>Atx?_NKUHx16*ek0^!ZYOg>J3XOW23H%r-20kil5Fm(X z&9W0JqLA&WRG@&H*sR0s`LF^3_JVJ&*;gaxb(|QkttIhKH&9JTLoT=tFIPJ9E0Ft= zZG3dL$9G4fd+dKF&7@|fC|?yv0_NM7k*b*~+wQ#mwh=Ebvre?g4;c!b$!G47K%zr> zM{^R*gv=n|TqQ37?wdDnM#wSU54VgWnU2dl6_%fKh_R8!8-eP3Q!(Z78e zAz~)*i}}mb)ed3@?3-edOaMyM` z#ACV6Y8}3u>%K4e0!a#u0QJ-?hF>BBajNaoCj|hq?^5rjU`T!D@wIRMYAM9WS@Zsa z?l+gXJa+XjV&;p=uvdKx{YUCoj_ggm#{Bqd``K&SB!Y!i-e5=-xvE#@i6s}cXTU~T zXD#CD<>GEB;0rtxvy5OwAAnsOMu;bu;U@F$s8Z$eMzC1zXD?VmA>df_R?NHKTiS!| zwS`I-<`ol}`46LWnnGtd(i)PGymh?XPMymxA~4gaN<2cX%?P#C&fh&)=;Uah zPjY3Q3cNaxj`m`44U_W)M83+AH>Nnv(bq)2-=*&e(>}7R>j8iEJsV|!DzCzr65@IrnJuBf;)(!#d$?&V_l%caS8Mznvg+ z&kVL?f;uQtTAT3CJ4CBw^S8^cQq_fg{P;1b`tA>@*~L}2vi2XghWR-npO5xuvRg5w zwN#`{i61(Jrz{Th8TyDGDs?$}5swqAHj$H|V&d`f%L-u{1!s9qH&|J5x8UfGJMU;& z?KJ17)uWP?^IFg1)Dtygz2*!!iDl?35wXl?s_z5~FpK&s3bz@Bl4E48I>SKQdOu>u zHj@dtJjMHTZ}BO&F*$92maA$bu$ZQEn!ScuRpe&fqJ_y+eH}mW&&K8#;a=0$rp8pb z`E_{&DkhobSxIb^PrOJ34`zCva6Rv#`(mwNoNts*nL451L$$=f7Oae8@FOj_4R|`} z{0`|`gyc;$!MK_Vz%~RPvxo%ElT2DL9KTpHfeST^4%r2-j&NjAnbd>DQ zfmGG4%bVWP8{)>=n8SGPE9fJ-&4J^(?-y_IPZ(QV^d7TnG7K9Jh^w3i@n)%VWjW;t zc7}+Sy90cSAB7lzWlTkgt|d5o|(~GgWs;XQ)wT z9C}@=q(FW%7E{_a-Fop++zYEU3(+e@H`$+OT?IeUi`<*L)~jZDYs>b={%;(o=cv@^>G;a4FaFf8Tdf?Scj ze{UES1mAk|iO=)FY&(*=$Ey~J+NKj3fyU+L@ZM+rTf#N=MOfk@+3 zJYOpxHYYNr9J=b6hA;FVT+aVD(gNg>9K7yyM)XH z$EbK$iyfyz&uRCCd$4>2@FQSGF>v?%1Eph0Iu?!w?fc7#c(@U~RJYG=p#alZotDNN+h?KNor~$U9(fZA zmm_Ta@%AiOjmz`M^$bl)vUK-h^7yUoHH8mS7fR)X7&w*h<{B*GNfeElE}HB2e3f^S z=Z3@zt5vU5t{Y~)wK3{79U*_-Q=wfh%Q9?&9}q4tFo-Yu+}+)AGt|=}%(&pSmcx|9 z+{ait%ZA;Ioz3u%KL)rN@cLd3)L1q!L~GinsAhN(h;%X14l~sxK40qf}*9lM00l`69-tywa0m5jXZ|?aaUGfsPT&uP~aAR+MAcX_Z2P+6EhYFyn)SH)&Rp z8S3W5Hh<8iZ>U4t0oH=r=)Et-V~X){-xwbBK(_e^eJN8ab$?^yflT94?a{I{i0+O0 zFsnD4*oU(_w}n>d4Mw|EXx^r7Tx@)%SbLg8hCC8yBf-7O_dpOf)mM(;E&53wiS8Nu zTO{wGqutD;2N%;|JM_xBDwfZh1Np+7;r(jMqxlb7GKq<0UFs&bg}`J9VN>F;Z(QoH zG>MPvZtmGp28@_!RLm#nTv5teWlRUj7_-2L@BNvQH{?N>`Mg}2{F;Zt)wC>hemZR3 z_hCg=4T6S54-C%N z{{9U7{KG54AZ!r$OrZNMy@3f5cAG$KhM|3PT+I0pptoxz2m68cs zI3bBh4Fmzw?56iU%e=CF>wU{C+i`%Yv^q-;C@*lus4S9Q{TRf{s^KKIW+Ie&3X_#T&U-l zeOHt4EWWZ}g>*Z_rbg#89u$wjJW*xC-Z+y$D^hs>1<1st!QVv*bKmujVJsOx5Y$># z!x#K{QJ|sq!#4JkhhLs>J|bU%d#Jt;rVYb z3*DvP3aX4Ba{6gBC&t(o$^eZcryAWJ&RODl%w_Tgt)0VoT123NwKHuDU+*rGo-(ya zMeqrl8ps#VTihKrGZ-z)=9-^!M|k;k$Bn#~s>NpC6=V6#Kh;ZM7Ji@j9H5zmDM==E z>UVLMR4J){CWrLnFuvXA>s9oEl0Xr zSeH(7ypLZY($YOVPt?8^}qr>y< zt_RE_CpvpkrIDBR&Hvx}2BJ`BVv=pk_5G@~wYBjbR&qvu;a(+02MSOjaW)&_{v(sd zala}gR{yGyTtHYZpqvo7Z)PyBIL368)bSr^;)*UIphP0pxI}eQA`$piA_@6-i3D`{ z@MiNP2Y7($26D(H>KP1{4+NYwr_~L)U*(~h|5hGKfym6x&W57W{eNDyF&-|St})Rn zAQ+kIzC<^0Jkh^7qK{!1vp>7ex3fi|GkJ4d?HSh z?$PO)!pjM!*xW{|_R z>NC92yc$NlvEtqsY#kj@Z7(D>yZIN0!{43OFtOLmJaG-&`uVzA1_h!#`o zPUefYhK;C>fmr{#IFb=HIDM{ThJGB=C$0xG{wMWIa1x#^?8&@%Q84oN_oLV=c`ugL zs&l8gj+v>|Np7@gA3U7ZD0-IN6>C&5f1GO)s+ahHd4HZX`LQj8}x^i>3p+bsB=)-52W(T_y8^^K^B;ctZJ3uTpmC%k-Mnxr$utaT zninNt;p&;pVg}YvBVh5Q{f2Rij*r6QKTF<=&sjPnABbN3wdi$Ig0;S76nH3})YK(E8YKpYv?k&wN3E)Ut&XtWY z=PSye*h9o2=UnLG-JL8GV1X=6bPT+g*_QjAqvPe|;^G1&O@geU`k&@cV<{)9BmLAv zH?GONKPMs=u#M#c)Jo^aMAR#@XudvLtN4&Rf+Noda~1J$4x~1 z*bE$ZmlnMMBg36Vm3CK=)l-TM@OIBH9B`#P0CwaFJ`Y?P-TchIUzC9$4+kh28dm4& zXod_3Wy7USxD|P*BL{p!-<=u60r5s&y2#K__R#<(E(Zn%h6)TRz*L&Riwn6x0Z(#E zV!AvxMQ$FpFOF&Nf3`iyXkRXL{mgvO*VPbMUFIvKqnC8cF|I1HFe}7f*6r)irvw)2 zQ!6O`l_&qb}`$t+laEptIS*7feGNe2g_gr#c?S$NXyEj#})!5l9DI8+p}CgQ070JKP+ z88o1b&q86aO?akG%IJ!;9x1I-sAL~Ld0pPnmTA$c;=f}hG#7}}rF zlxQT;6&XBk16&!OjK-OR{FLi#v+iBE*eKK9+#z>noOuX;Et6;!yi^DHL3LC*z>Q%{ ze=*O29FUCFeCE|4w?UEr(XEapy0h!$Tu28=!{ET7M&|3+BVN2f341Orq=v0~_t6uI zbKk-Ncip1vIy$>+dWkqIIs!MC;RlqJ@gNRK{Xdc`AhLOZgdC9ZEMi3ZOmUPh%K!qp zq~t3^RWVa80Zc6L0W`UUJV4dT<~*|(G=u0^y6Jm_hQ}uc7#U4bjpi~6nUjk*-c?4# zo+c#R0OR1(FrxPziLL(Eu+>fej=Ri8_aMbO$e_-$RlBS0z>i2;>fa0Q3AM z7IWaazN4>I(X9r;nEC7 z$=`qte;rlhJvoLDK@>F3h55&FFhR(@BuW;&hAT`Bhubw1ZPknRq4^c#cY;LbuuO*Q zwGPc<%1;AuEFKn5C2U<|{D9QdYb?ynMfu{rFO@#?FnSOo$*$i}q)mP3t@Yg#bBU>y(jmeaj@;u(Q8>dD`C5aSc>$L2C$g`5!J* z|4XGZFMyij?yU#Wd4$%s28I1Gh~mva`LeFAmXz#TcNH`F*K6Bs8T6Ge9$Bg8-44*O ztm!OUgK>06;g=U$k?LMMIX8gw6wrV|E1ai0L1fT_d4Yb)SQ8L8%A8|%B~POP{#jso zu50lu39Q`%d56BN4~L^xss73iWjQBN-&4jjZ8Ap5CTe^JJTFGe>&t8i*T{#s<^+L! z5}zbVR}u%*PHnhCPi05rag`Jb8QYC-r-aup;8c7w!+-LS2FRpuztYIfeWehd?4uF& zr)%WbDJUTnF7EvpFRk*!hvG>IMF6M0yB9wgh5?R!f4ldO^+N!CL`lbBhHJRD#M* zF!e&i?$O1eGsoPMj)pLxm=B=3mo8r(p`Qe#t^rV>`_N9*`1Y`cZ8meYp&U5?IXD{4 zK`#=QL%U3d5pvLPQS<1DQF}W)UopsMrA|w9|C_6jTOjY4Y=ci?7MHwM5RjATxICOc zhFVb#sPqJd7HMOa2~BE6=O83o4d?Xa#7l6}6_L`4iXK|SEjvL}jmG(c2~gFNWxh1K z2OKL~)P8wd)A)UUJ49q>QQcr#$L;90Mcp=+&{0I`$7ol4;q(q+I{{Gr(lRY!|8s0I z2EZEF+1Ns_`erR#+dVQp_sk(nDA^!=29dE_8CPrbB>Wmc??H&l=-KNBB$QYEk%WAU zF)t6%rw4(Cv>pm{?1p~=c2CCZz>C;p>TIZjS1*b88YVB4uraqq`#af27ijGib`GB2 z9>DQSml3w-H#;XXGX5@b#(xZ3>rzH9@QVsAbCBeWJ#!sw?;Zgc0KX zm%x_Re62r5_CctbY2`EC`z9A9Y)pF2yammPmfZ)0PYEA<9@@)`9)+)wT67q%?EKVm zBf&%JiWh~bXw*fT6}20dG=Hp-)pol*kjfZ_s@E|UK9k!m7A5O`$YOJpaAWs|y39U) zf_}mlU&xJ$k;w{L61!Q$nbcP}v*84jHwA7Sg_%4$;*)(;Vi0J*ylj2dvRd-}Q!y7I zmHPcTwpd=9442&zY5O<}y@W0Pvcp#&pzQ_DE6{zTk1>pvK~5vvtF`-+inS`69>ZeX zMs3G#2iiwGjPCM#_lF*r*4b)1Q>s}uL(+@hmTHNHilK25DLg&p%ZR^3l3M=wV$XCh zyM9QcrcVDnO^`i=75NV$kE*Q|*06d37$Mw+w5VjA(HzlzDe`UKRJE}pp3Z*Wjn-rl zS}NJ^AUxjsfTMd|t6E7)Smg1fOC8t4hXzl$pDx=Cg=-IwUq`>8t`d8&+e+Q`*>h(k zBQfUz4;|mcQ)t!C^~H2Ym8b;alFchu1fCj~sV%=4Hj+KtF<<|{MD4V(=;SY0c*CFMMntiW%R=C=_ zTfSX0EIDrS8FM?{OGD?p=OfBq800eaG1pzaOcPR_S5x1Wuu*7dMQlH!M=g*k&B0H$ z0dkfP$Qe&VWkTS4uiYY|$|@9>aLwnRrqq)RvtL4q)cfW}l8{h3)e2k2X_i)^y z6+Y?2-f*NcqQ8+}`^5a-LJ+JVtUa7@)XH@a&)2x6o%-NbcZA7)f&+h(KdzAFNUfuF ze`bMv;;BBr9Z^F? zLGfe`{%w)gW4bw+^aQi|0x~KczFfTQ)@9?Y!mXB($e8e>Bj=LZgAV51-FcmK$5O+R z?-^G+38g7mZ)jBbRt3P4<$LQy_aM7_iW` zhH3*BkpsZ6W0`K7>JjQM8s!#r4#GdO;5P}GUz?yTT)wjYNy2_}Ei>K(2C?1%Yy)GD zFQvhgmAh>C0t>fB?0q7Ukt&z?bzIqZw(ENF6Z_LDmr7bEEy6UPw_vB1i;dcSV|Ebz z5yL;ZWgK$H=CN7z2bGdtm9ba~%1C8L(4vXdqUBl;x!US#&Of+6Dq1n;Ggjcdg=vp3 z`h`qjdxm>FJ*yDF-4f1I$e(o$nC}a+cjy+_dLHU5LX0zAxLtoVwOY?A5AU}qZ5I?v zX*hzHt-OK2hLkg^2*G>qLbdA(E5zQ{Sgn-rI~vJWS}?tB$|@vgk_+?il2GP0?8@Uh zY(m~?i_);rHmG%f-{?afPAhTmAkFMpPH36NVce~BCs`%(N|$eu0m0XdS?ovKMHWvw zCL<NQ=c&*YK?UIE|Ip)m5)wU)cS=Z^gHl0XHvSC2Md2o5jWT@695@1pZ`sU=S-Ec$K@wb*Sj!UetuQ&XyGf|9_wK0gzRc(fnP-y zrNerAYv=AncaVU{gOTRyR|tVDGO*0CxG7Kn$4>NkAl&yu_GnbP&KVsGL(^kLRJvzG zydaam$wp`JDcox*s_<=bVYXA9dh#VU%+-cPl2|?i>qv4!UVzY_>COe1a5)c&ak(z& z?`}5l%oK{Xj0I9S4Xjyj`eiy@rP%7T;Toq$a+uKGr`9($UZMl|u63^I+1Io>ZxGu= zi)=07=&z#U2UYly2h({d=y8#p7PopXSx#2gev= z5UE){i>;;_%7Cpb42!LpCONNM`QB5n73sLyl;*l4#AEnFg7nr*2|vNgJB5#=!HDE2hoT^t%xdyC8I8H3qK^IMZE%@HDb z8ZjmO2K>iWl48F>R_;B74xeTg-T=IFD-9Iz)@G8C*FVM#qi7Vk{m7n|k_FT45^icE zH4iHO1g^?F{K3)7&rSJgpmTjyVrSCARKVqGymG}SC!rBTp9F|@z(gva<1E3Qc#FiS zQJaPWgTJJ_D7H5(#mRTD0Eo5^d9f8pG3$Wga{zhPZKKMtRS7h+@I0y z`$@dU9@?=n2S)X2U0Vz+H`5AiSS;Ar2cxy#YfZrUb(r>->ZBtb%SJMrgKyP$JU{lu zryT9H>Bip*Uf-Jn1E8voyftd4jYXv6gemHrbtufkC|tYR`R1ht{Eh=%zJDO>t1ySL zmcG2`jc*-TyX>nAALE{OdxihQqVSJX~|4hUOp4Vy_%-J zNK(f_`)$KX?nL9Z<;`9`4f;87p3036z^s)%3dVmsJlE)3@s4-MjOkUv@OA|?uI@K3 zr_|cDQUCRKzA4)8%#N#h#9BT?>u^xw&=JqS!q%-dSa%gZ)Ts}KT7G9*G=E#{9$vn} zl+B>9sY@nmHDB}~FVm|(39uMQ4?vg|P&XZgBScC}L1UA|dQiv1_q9FFEQ)t{oA=%q#qag*lz@f>-PJZ+~2*61g=!WHlF?*<$uO z*QmR5)}MoIW3g)2s}OKtJI2Y3(53JH@&@nDyn(XZHL2fDy6D2Mc){#&f2R^qay?Vv zdI;^~0@^153+^*aFRNJ#IwJHdLjB-Jrl|+t9vo_XZl|9ef$#cG!@@_)zLJ-zI^1 z_Mw&%Z7f?X5CT&h*b3d#%mqTXq%;cHTN&2%e#Z220JnB~0IsqlaqvCUA-uF(@Ww)k z-vQa=aea;cn0e*vY8N}P_m;J5QtRK6F;m%d>Ha7g5F?H4j0KX7yH8Pm-x!ZM1i=wy-|(Ts4c!v%#Q<1ee?z!zd1Kw zX8iOcLF;J9nJVnvm-3uE=z2P~wQ~2y($D7r=DaxWil+^RF{Qixw9iWzI1FRcZ`IoV zVTeQA81rL()mm#6XSo_*@PJI9VC>4IQ}ZTZS*Ep?{874|2t>HMV$4Kj9K&Z9M_~}h zv+mFh%LpHpY^R)_lhv=YnA~$3yxLX|W?2;0?>P-XHmf(U2`pqar^@w*er-K|0#&$s zAEiKmIq}Ng+nL$f7R{rr;i(YON+mw-tMMkmBKF~97In0@Mz1D{xqHUD-~pPxf3g@Ge|=<54JnIOLn8i+Q^qjrLSkAiXHz<*sMFc zAmnBl{&|1EhSdvS@EvvIt?!#Q2FG3ml^eUq?>jt;ZQF^eesmGnGUj54Dw~>SD7l6~ zhs3z`d%!G|*Xx%{dNW|1gsqa3SbLRk4#8nx!#&OJ_5vV(*CE@&?3$;yLK>iPS590m z&Vd%xKLAs75aydEbZc+Eu<&TMGrAKMl`7OI$+c7;78VKxHG(a78I{lll{i9jTSFz_ju zDTVBL2&#KtDh=3Vf|ID879HLUn>yMFztQ}vt{@78Ipy~p6K~3+2Rd#^3c2kk*oWgd zY0bik**vV~;lrQEiAEUj47&F`Y`@7*dL#!`&~$Bg)J|@sZY0~;BDL-|kCeZe-!RQ+ zZI>ie$Cx`VI7xMwBfRy^%l^mv{*R%k_yX@BVc+{R+6+kJO?({2?c~2(j3(gO1JENl z)z)XEpWa@RlECL`xBV*GNkGgtUTS=A!a4!LOv+4j^;~joY#O~<3aTaH-lsjVAaFzu zzzTYaNrcT4Z(0D`YPWT-T&dQ&3q^jv0GxB4!J=O6(1jQaqELRz$?@@r_M8N&Zy0K0 zFSD`ggRUz+tlk1Gq22>(-tSR2G#K5FMbx_Bezsg=7KU*j@@4staEF92xn0tN> z1`*tA|G0z7gaz9WH40g=KG`Xi;x0(!`EJVCX4RwMZIw^n-SOW&%Q}YL zNBCRT0^|UEO2HT(?RB>B=k6lgS2t?xUWt%{G)XX@rb6*M(N|wG3Lh)1;6Y#NH@NP| z7SLT~HkSZ1T1s`?kNh?pO68N$lrg<{DQ$y*2`F*gm?;n)j-ftiRn_Ws*W=+#hKrbx zc6m+{a31hG4@CDr=vQ!#1!?;Pu?8Pl#fPD>_BX0i`k04{!{j72D^-Osc)eVLu&hPh ziZzEX;muzDXON^C{lh@MwLrhg`$%-vIC>|QA<(YkBm{*PW>#W*>axARmb!VL84@vrg82O{6sCMLz4PWLm<0QVw-}8%>wJ z=^DbXcH&UYrQ&X6Ym=S+r82tURYo6bo780oKoY#ErR}_?=IGo7wTRfsf!wFGd&g4J z)B@ZXnBhAbCqGq$TcM;29Wpf|l8IoidbZLU<66y@%{Q znET4UJXTqW8~|9`vimFh)Ow)7mqua4#5|zG<5f`US2)(8szn$}&Gco)qT+d~8YcMd zK$RYEeBHNP*JH${=0YBq=oin-&B?+lpm!lpm*?bXl!H4X8kjnk)Q)8ULP!XmL*Q4Gv^1$0=KzzSmG4sjcF2cZ_G4sk5+hsTE$%eo>+5z3d#SSniz89JngX+r>XiC!Aa}(B;Qti z{Fqs7&OsOM8nq3ccI@Fxxd9ryGH!z#-VS~5y(9LS^U>n-+fTWNpB4PZo^Qz6KP;W>2oefJ0TERMi)_b` zHWmqR9_=*f)G?;>yiC!8fGoI;t`Y{P{iI`Sx~Le40Jf%c6`!M!Da(+Za#HSsY64;8 zo6*oTsld>q9WkZ(+qihznl8P&h;;v8l;@Q*b8!(QQ(K7Tg=qma=QJU)W}u^!=yTh( zt0ishQNI+K0KL~#k?$kr~~a=W!sR+e`DeLv6G&>#&27zc=oXTWXHxA_b2^!4{Mm>;aV$E~rUUUrhP z{d7ox4+TNkTTX7--PdjOlR{<<=Ia!L%nbRfp!FHTbtVHyp33avNP3afC>PFgcEs}$ zwavAGNAj{yo}_?j5PopawS8*$57@eSMY`lu&yexlktAC4vCudxrYvCUbo}1v*t$I4 zY2xR$oLn8`BqjVyMNU(2$Y=|42&*NobbyyqH!7Pfd}@K=n1 z+UrDueIE8n!VB!GH;DOqIX{}6Hb;a1?fp8^aCuP{05r;EYr)VghUG$RVm(l<^LV+; zBfh5d5eM!MctL<(68FC5U6&D5Pbn|NykfFi^x&#l!a$=jrzzjdQqrt|X(0A#A__}7w5~D4=LdXFvcEZefe>4qHBAOA^X)>dVv+{_C zxa+Sn9Gk2$Uci?xUm}qM{K6Sk!FX77)gy5HY#UDj3rv+xLZ@=l)PL`XWsx1x|I1|| z%3;w-ZhI!M_cfsT`6(>qng^jHBf@3sVx8_?vI$~MfrF=rQE$tt zn8{3sa1ye${RqWak1*lQ@k~L+J+w?E1qX1M-LLY~nub}uNt)m@-Wy%=4c(f!rs9&YIdBHsQ z+r+|+@K={lEie|3py&Gfg;)V-ebo^pdRz8@Cb)$X%)($?``F!Gc@qopJNwNMBp3Hy zE|cFR7td_mv)|K%IH7>#WptH+KfPFH>?MaRGbo?hj`h&es}kdOF137vAblH=q&%Nc z>(e*|Yd~DLxGFJ;+aYvDpO>3pj%jD>peIGb989Ct1yfECm2;}D0|&pZn7U8nOFZEu z=p#+-XK@bbN3}-T=pgBQx{-EFo?r52c|lFK&fNefs%_lSpMj|+w#=+)%SDm;!G%+R zND~s`p?%D*ZbsF?Kiq)s2$(g6yqtoz-)I0WD3X^mLk?!4?v-OE(J>Vf0cF4UO4CNc zLh~-M;_UHkBWEm~?SUbT(Sca;esq4w6l2r~v6o>wk&`(9Nm~$eaR0OMr^|a!v|a0cG6A!^(SAv8F=^O?pfCZARHm9 z?x#v>+pw5LhZEHZIDfpOW*qnG4T;n}3ORd}Q%$D{^2l`!MGyek9A+XG|S|=jN{!6K-$55njFhO6WSfM~VQaZ3Ez6 z$2Kk^c!v?O{!5QvkRel?Hb@|&g=sgUbJ$7?mcc^@HF)IrT-R3{Ur64cof3!~-axx~ zz;$k%QThuH9PMxQc^0PHGqL1k?UN?NioY|F_V)G$M5`wKMscq(j=@MM=)Ig0?CeO$ z{VKzBTZX+Uyc)*zrJ^eh?Tl9$YQk0BqI)!?jTKjg!;u*1MBp&F^J8q<8IuJ#NMh~| z$CYW+D9MadE|NvS`LXNZ#3rLAHj2AWB1u5{u5+}U2|SM*GTiazlx8=y&th4T)~ZrR z>sJw6*3GTH-BQ)URQF=zkA0=@!*OJfjMyvSvfuCG>Fe`$M|2rpLF3LbzIk?b$u}<; z_dMM#u~sXwts;HfW0*Et374yiBfWi$juYJx?>Ogl)ZkOW6A~I3$pYtsZwWUA(2$-Z z4Fx_F_af5u#qhH|NQ$GWN)jPZK5LP|{4M z{Q@PH2Eg3H0N%TY-Ml*6b2?EfECjHE`xUbXUMfA5N4;V*&k_F0zr&!|@kPE%RmN_C znIYRPj&n=L;=w@9Hwg`mWgN-Cu=-7}9nwbuj}9#2Cvs+gQVvo)Q)75U3DqzyF_?&EBbJMLgiGo5@n zAa%9xrBb={f8f`%RsH_dNZj-G{g3Hp#9$^qkI>lIY~+9uLl=W3mBRAwM-||yli@J664%JntxsuYyuG>8JE53WlhR-dh z+5P`=n}@+DN58V5VcXq#?vTCLWCENIF5@lRRv&V(W42td)HJ{k==iSwMz0xfD7W=X zsp(&KT}Bcy0T0AjySuygJAl0QMZqxXpGR}HWSMKo`FB&05u~hBo+EwlT3Xf_DEjB8 z^mGK7@fR7&WU|)L03SCl9nM}7k2vw13IhP3VY>(`%TYxy51(NAVOE)=Q$ zbD3vr7)bH}>idi4_V^{XZr9BVge9@@z}bdkZn2qc+CC3B_F(p;3hW6 zk)m-bt}&O&|L=RzryzRxA|n}o(S&5)>B)qiGeBqi{_B&B0642@Yu)t2hPr(Z%iMaB zTW;6Y)k#E}O}*+{M?t}PeFMVAZ_ngo zuAY$s|FI(RMgtV}I}0g3RADmocL;E!MTFQ-3-kZ{E|MocTQb1=#7y^?FNFan$)c#q zmx(9T{QTzt@IU0;!x@doqqovhr@gzIG`Lj)n`K8>u;9}FG*X^HY(G}rkHh!PN-P_Raaz`bvq(hRFIlicl?JJ89YK+DZc*KFaBN; zjVlUZhAKXMNYkVnf*Y`T(?>q=s(6x{zE#maULWJkAMxNA`|R=FJvclo_2U@+W)AFI5}F` zOC>N3GSV|NI+}?p7_d*t;|1vdw`ySVJZk*dh=qni6etBkIOt`54pvshgYPxCIl&x17TUo(kL>?u(^W3^;0AzQ2M5YHng6++ z_Akeud>L(tNHGp!2P841*kY`){_=Rnp5oU@!VwJ;fcXrR=$=e@W0=?{dLHV!Moo%EdA&}AOXOCYrB`c%9CQ{7Kx{2|ZulF$~o3mv+h`pBbu&tx< z$8%wQ-E;^H^$fuN``qs#sQN^Th`qD6{^l7PM(%ZX`+t3+Sq8=sG){Pmdk`s_49}sR z7v+EKOh{PcOnZ1ft<&bXLS~-&QTA_NS3m~W1%Up!Flh1)G%Y@B z9XPw@$)`Sc-RKZjR#qY>-Sh+)@#*sZy)ELbV55uf%a~wz%c(_ns>W^Igwh@<&zE;r z$_ZK?f2)o;GE*ZBlzJnmQkpmBjZ3A!+|2P8B@w4lMht8CK3tge@D8i;%`+b0f4TQ4 zI-`8zb#TT9ZVi#Tw=twf+Xen(hVMTX&cm2|IdO?`auij93G9JiG2UNID%frg?O~Te4YF z6x4sYwjybfv+m5yjBodR+DoMc+kmqS*uNYj@EX6pz>odIAr^$wF!`&~U_m$;u;bXT z?u7mJ1GoVZGh}DU#D9oC_|xQa5jQ7_r^nqi1r#!q!MV@>KAO|#?MX)%J=t(ZSZ5}XA25m|A(NS|5LmM`#~I!EMi0E=3aNt)1jtKB%(2$iyr^A zcPsb7(}1&ij{g?y zGHm0q*DQ}RWSB3NaB&J#5nLfN>7Q%-i|~bnAp)3}O7ujbVD28UW$7(6e;2%{kUk_S znv2OVl}5||YD&`SIUU#abNwo(R-*ww>YmJ-|Kr-Z9uue%+?+YNxnQ8FGR*65&MO>c z6t$V1_A)L8P!^WIDvL2OGQ1JXr&)#AvT*o5h7>HzhUhPkXsp!8P8$j4iT}cTY3gQw z6YDaZPYmgx~Td=l}nO;mPUgclWjy znIb~&DF;~n?U3y~!CN)&(-RRkOj$BK4)F}Nx0d_2(K@?AAhz)?ZzHIBq_b8 zW~V(WXWD=A$&-%|gd?DG^v86S`3W-pO-7~ALGwOhm^nZ;gFZ0a0I8}^zNg0U->dpg z0ze8m;sANp&r?G$_CEZ`3BaIeHIFoz@7bozHb{Bwj-jDlwE-U`e zg^1q+_$LSK_??sxKxy)8{Yr~U#~^_57sDW~j=qE&AWlQmYaD?jrCu&@>n}EshvlOG z2mQWuSw%3h-$3_yoi?b*Khq7x|0J*LSp*c@uvfWMtI>BJsED_d!g)$Y=7DEWH|b{SO6x5 zEcdsAw?lhc90LXkfY(l?fE`!3lM{rGb0(wz`qX2Af|v~qHU!K_2GL+!qj4ouliy#~ z(@5xR`6VAxSIQb)$P;_zCRF?&_v7OKQ`uL?McK6BQi2Ewib|K1bc-~Kbg3w?w9?(u zB?_XH(hX9Az|vicG)oBV(%mVwG@QXNRQz7gIsUc!bN88;Cb~Uv`OjHn@@`S$jTf2Z|`3E+G|-!mzcBYvb9xK86VJUDK>%{ME-e)hl={J+AkCqW~rjk z5?oBnd_T>l%UPE@To@Q^wen|QAroQc}r*3fc&m#uZt_y{Qg=y{w zyggSRfs^_{gbO4hUpI69^DqcxqQpD4>!X=LUSPXkhKQ^A7k2CU3&Cd&2ZWWtDC6QL zfE`~;<92=xet8GtESG>QOLE=t8&p>e1lJ!}{Qoxe8$gX--*xbD!2O$3yN9`yqXv4# zDB*vN$7exKqr>zp-IRk4irW<|(tmg@_(E-agSp4YawVK)9Z3Y@qy?r8iLo z&cdxE9p;n=G9WDe`Xi+fTTlMCj94tk#D;+^WbttiE-o;z6d*u`IT_&p?J1I|t1eur zA3ECVTiV+REtfzmjq5%F*ZyW7bnDI0Zlr_Lf(g8@P!nD`l+^vemA}n9m+gTO+L_Y9 zf09e)==Q>Y8&#rrWFT{b>RGtBxwVy>05UkJ=GgxrIEye?K8vG#7L=x5T&v%7) zJsca@mN%P^e%cnZX-W| ziTUW$K-k}YpLfBhU?*`U;4jWa?oEO)lIy+cfKCefOx&XBXU-u1TL`N`mrO!5^i}9; zfDFJ;?YWC`b867Fb^gUJbU#DwMU!A=Zi{Ca>+kQ!;MB-zLp^uz{QB4AMzq=9ObC72 zk*Aml_|L8Xm;Vf$k`L8=X9$mfdADz$2>fNf7mH=;mv>rIw3CVW!y?R$B~N*}-iLTZDX6cQ|Rh z#i!A9R7e}=H{=qT0^YCh#JDI=3psS>zWoGm(`@$)?NU>1`{HPB(4P_X2sR#i`JlKW z7f$d!x{hLauTti&r?}tqRT^41D@Kpaqg>ksTw$q>@KEM3VCI3Nw_V>-&-j)8{I&}% zKqD7x`5mO!0;qfb96s-edccMs-v|(_6Z)9>{1I>PzRNbaVJW9*Um#3ch%>i zj*i)Tx-Y;bfVEJABKC5&&3$w4(;WYzWuO5K>!o!marn~S|UKTU8ks&M9c(^PMXW}sWX8em&PA(>2T z@YDp^xIUiH|Bw=U*T>r&!-|xvX%<4v)r+p54fyXp=nZ`9g6!w(H9pS?l5L%YlI`U% z0>!0G78Iwg#@a3xhn#)RB_Re$R31);o9L+?WAgFm3(`J;H`J>=6LLbVIv31mrU({Z zqg9MqEQV=reNNC^UyoECi&R!)wO^jKAH7(vs^?smXgBzMRNL)X#&wduP9uQF|vw639J;kUBsrMJ($ z=y1`;O51J*`R=e1sl)5dRb!D2yuyvc<44-sikN=pNeo2iUy3;Cg<9<@<{;_$Zs2uA zi@)=VYr1WGRJs8bW#QWLQUL#0ibhF@;%w8ERJJOsxnaSz)regc;6zQ3AFP0#rHV24 z;QVQGxCT&Z6tl8+R94q)>`^2dZcOVYEJ{7kBk{RR!)Xik)pDcK;0DVH6fbgdanFML ze;ONbxvoS>(00EBC~OAr0pOtGT`CvwetK0V?L)P~rAwDU`0~ccAk?R4BGu@Rs(*qS z05{HiAV*vdf$ECMsd4>v>Iqd;ek{&s1!llDUG_JfT7~mdBYx_V{LASQ*E|E{;TU%M zh}(g=v}&t^;*=r|cybB}@ocX={k2Q7KtlG19f|dv2|T|cled5*#DaTQyig}O_6X;buXn<={{*J`=-Zpc42amA>E(aCd^^JoCXk(%^^YgmLmPP|L8v@F)3D z-+udc6?~uIq6{x7z4v4(nEv_7+h3;dr1raO(i;+g<&XC-3g(=!9%J7eLknoo_K6qN zUU&~GEa~ug^uZx(rP0@9s-muJ!#qvU6UDj{_hw$YioNOC{Ta;RhY&gF{Yw1MFYg-+h_y z`bK}0?h-W5Hk#+jh{qxL6%$>r&S3Aj3t+ly~Pi~ zjx!N+>kRZBK6`L?Adck}MZkSJK)`hur6M7=@{+JSKL4iDB*3@@9}6&`Wr45Dzyg42 ze4tz3kBVzAbp5)a65;yNk{JLNP;r_6>g)W7WxP=^*HJNs03$%^aM0ceV}$)|dk;k^ zCmBVAZXOsAZIdBCiTZ=+9>iD- z2awA)_B@bX6S)z#KJKo0qvj+s=Kv`L;q&t0yQMy$TpI8)eWCt19T6-wW1Y+6Xmz&V zs&zsX;xt(QRlfYH@SXiEklxbRdg=xeEapK>|0BclszDMiny{~Ck49YDJ37C@N{i`G zL^OO1L?5ce(Aa+udrj1Q6!7(`cXy)T{##i$k1fjHai2^y|=Lx#%JsV-a9E_6gk}p13(EF%ZC%z=as(_O|%>lWsnxTt}1CVbXF zIBCws@oUeMcIMfDuqJ`B(&_SE1ApE471m)M(iZ1czc|CmoI!ZD__(OuLh5ND|JD%p zk6ZDom!Y9b?pYlq|0+vMy-Y*g9l!)hL~dR9G@Da3Zl5>NS>zNsiUsGOEitt(ss7&c zC}wAn8}hh92X6UV?sRq2P^Qj1ki1X(`Nazf`nVla3p%B&NWniFCA`G zoIk0&`;;Pc+R@A;XFoWl15z$Nb}+@mGEiW*O1d~&68EyBpk#9A`^hk3G4A`Anr<88 zcxOp)7M`@Sz=ZgQflJQpSKT79{Q=(YJk#>HVR~s^6*}A4!{eL#L-D6%TL-?2Og(G_ z;j4$pu$t#1gKf8D$&=bP^}AraQUIOg;l+KgQ{?EDahdVBrwW7UR(MHuFA{xbH#2e4 zLf+5_nK_r(vD10@w7_<+X;89AU}Z5J-IHBM<$%`KthFXjTbG`c7xH~NBr=S$%DPME z!`)~R|8=_B6^nU?q5Pv+{IRh9WjI7}Y!y#z^r`gX(?SOaL7Ux&B$`TZQ2dU+3a&4I zQPYi5JKZzJnpjv2Q!xEFlZ0v(>1Sj<(EE-)M8+y)4fzcAY5(?4VOQNXk+q?9qV|Kf z+4tkdnv3u{nM%A7ozh`PA}(DgCuEP`r2N=ny+$YZDcibxXUqp^YsR{cJA@fl;MmsL zMTl2Qrxc;p&uH82t@KcPt&qJT+HP2?OJr#&4 z96abYE{Gcbm8&(9kJx^=j|rY3On9eaerjCS3iPzISq!mjNW~YZ;ZhoK@)9uW7%=cR zu^II}bt&vIhf^?~j6eBCKRi#Kn!Za)?Q@=9V^N+|TUxb0bIt~@B%8_s-uXSXTK*oJzz&IoXkUTZMY7&DaV zxk6~#6f71O6Az_&JCn852nGQ*!Qy1L_k@6WWRz z_!V=^Kic{vseB#vy?$-hV7x4uwyx)?h>~|ofya*T#&T1V{)MZzCHdR;%f@a#A>&uC z;_Z{ZSA>^$uq&3(U`Al@HJJv!c@RW4yM(n~hIXmb8`J1AX@!5F&_Xw6DT8kAY*A<3 z>J8&*PX-+QG8&25#KrVZM{~XDwYLm1iCH{j@l1ew*Kq1o5_YBMo0}p%aS8&un>u)> zrpi&C$$P`Uw<~;7RxX6E`KGo$DD%q0MVd#;T}#5L?6d7Z*Eo@~iyb!@zDIn@EMkv{ zQ8b5Z1l6yiNa!D$5KMauDSFX_1w6Dw%b9IXnByg|{#fQ`^jP?Gd1xhsi(iZFSPHSd zKdM!tlUS$sA}1DM<2_KDcvqT=rb@>qucpNu=SS>7IRgBWV1YRkf!P_tbhq6#kky{3 z>7K|_yuitfun6}O<5$jTj?k4`iFjyPn_)Wwvu?p>F+1T(V-#{sy4Pdz_5oDbWj`!^ zlfypH@DslvVq?2MH9f8lNoDT!X>Z8vh7~@s1_9^soi_$8_i=kRDs6j~XjS4Z z=F?)p2n>68<{dM6ZtM%r#+)3rZ>7~SvGm6;whltu(oRL=9EfTX$CIA>g%yN4JV0mR z`<_a2J$CkK)Q3mDWL+l_CH{zI+rlRflJe`0U&PiNVuOjcm)J5XoI`Q>NH~`Ht={j} zojh9eD*RIMt--T0Gagywo3q@zoTF9KcInz6dY;IZ#V6rtb|;>l{t^sz1|RNjb>oXK6>J9I9=9d}0<6O#E=xEz6yZ416kQb-`qmY9OUX6}&5x>_PE~5zy}reEEyAA!#~+x~oEnL`et)=c@hE^#s$H5w z`=H#p6eCQ`#h}oAQ^vA17W3`_m0j}8?i&54T1{#ykv*9N%o(gPw>Ss>-NrudGtYOM z!|v|Wl$DKMk(r#0X@d!GRN(p`pUc!bxuZykO>`|kI&8jQbYrFCg4?>iB6cPZq1+wN zNENc?>Y7)5ykp1m+O}sBeI|{|@ld|Q-8w)^?Wn?oxMF%GAt&)h<+IO)%mN$G5~h!- z3edp1knxAYwuTwJd$!v_)^`k(g9lTgTF(^}Z%7w{ULIn{rA`L3l|zBS!(J6(3k5O1 zd6K9Yf{|w~!()vivXi5Q`;2kaI=W&C<1hn3lO~_Yi*Gm#uZbzS-FLs^6zqOy-khYW zJoT*~|jF@VNN)>Zx$u7IyR{&-(rN#l?+q zm&oJ8?J$a)lB(7?ZvN>kYQ)RKE0Ptf{6vNp<2ds-VZh^a4(b%D`+St^JlnRMBQC-H z>IGM7y52VBx82ODx7r6xwj)u!ZH61e>oG}A=?lr|27QJ^2$Dk2{Yx>nUpV3&g_1;+ z{1V-(OR86jIA-dIPC5sl*K2zb!oJxsY+3GScHL2fM<X z73CvMVZ@;U<=Wz3@>0`U#o)Sx9Fag)o@5K^tDDq%e?li__^1W&(H;lL2<#{wx~d@J zyP&vZ?US+l8efl>&$Zmt-D63VX3#oQ2k&w}!+Ey=Jw9{!?Q)846Q5 z*~RSz`K?*1F865*mYQeb+pCGoz>ig~sTnXM$ZL=F*jSc0(2To^PHcF}FV{4>kq4yz z2=%Wr+Jxv?ob_>Gs?G323<0UPiODy;CT#s(B3PcuLKX7h zZk^{9F$k>{+ZL~J&%NdEV#Ahc@s2H%aQA24Q?GmUbBxB2j_y@^ZWWt8hbxVOsKG>I z9-5TS2iWFuw{O&K#OwASsuo!(;Jz33?03W)^R@p_9&;MQ06%sAAZ(=wTY=3VOWms} z%Pp&|V0i=rqHP_1t1&v`v2DirZ@dH!eoUGD-9-+VRxvc*?{2+6F+8@`Kl;Se>-9m& z4wJ26`^4ikvO-y;cY(sZ`{@8v^V_Z?nEdU=4;is<_9dcnu9V$IK}byq;@z2gFu3|5 z)`D5~;sacf#XFy3$~=x{b-N!uL@8b=HT=M)fV1OKY%MPqsSKyyma^(lLti z`|1z}FTi1E8NJCr`IQMRvd-mo$oN()$=WQH~RP+$q z)xA~0x@W5l`|xnj1J8ZenB8f?{qg4Z95uI*m#LU-wyEadWT6S06~c!nSIxsBgI&LW zxq{3O*O^8qcEull4$D%pTFNH$Ye}iHjQUn^9*a03ipr0{Y0*#564{;;A{y#cyQfvl z<>PCh!c66E4oRbmRMRl@kIYl2(PfXera*`$BH!Ja|DL)c86&#v#l4eGlwzcgo5}E zt%%zj%rL)KZ|Mxy8ke(9)0N#=QE@dttm*(Cq1knMiI4l#tH|Ip3$zt~y*g@}E|#L8 z3x;^hAttu?Tsup#M2+J}Cp(fo%u(!!T3&2lYfj|!15fQKjabw>LUxBpF^_6>QO^Y~ zb4a@kg<+cvBBI&A*aFbB3vBFh4CS+AGDLIr1=r`M;?w^k4jB+r8%Y8hMn@$R_L9ip>at zFnW1fd{D`jLnrgakwE54>8gNp+En8ya4J;cvZ*iI{%Y|;FgdrvS;`@hTrg*B2WZtw zzeirj;QXd!ae>D3=(|2-Z-|V0KH#)Z;AzsRbc-mzwwkeaf1zB=?g-B2sy)Z;!3RQI z-UUqERQ-}4^bUtHkN$DvjHJz zTKkfZ@gv>M@lXs;&$-BO-J_{x&(;_uDV=L`3kM#fG>EpwX2a}OVAgH1J8`*Bj$LQk zirdZ2bs{@;=*nWHM(`AUMl6hVDF{LKVyX)&gjYN5rpfD&^K~Z(D9hwT#<}j5(JpQG z$p~Y%pUaD`UAHkuq1Dl9zq5$7K-$+W%Y$zvxQpl{fC-jcP1TVd>1mN^FF!6R$cVRl zh9Xg>efYgjQcMpad#G=c6u3WEt`7KJwEu`A+Ay$sZPs;9)iaoMXfU8!h2Re^Rj zK5tlELapOiRi}KF!*;dUQLT0^eV66-!Xl{>sEjIH(9%y9C9U9~u&1uHCC|PDS zrQnZq_`IkU9EnX7*}d0e&+qic`X-Q^B+;dvI6(2+zv;~{r1>Q!RLgzi71#?KO-g*; zDf6|bgVu&qIDDtwJ+l!0HT~LSy(KNrI@QkdX$tLGg&_|8(*(En?ib0|EaNyv(M1P$ zvmr`GQtl|zBW~kSA$6LDtDh80O9r3YXM9DGLbxZHc+VDH_>|BPpKt7=WjBLj97FOu z2Er$pw)+Yi@d5AEqAMr1t%l$Fsq$kjPM1nG<0t%r`x!=3{Z^^b53jeCj#kP$g;m(p zEz+da&^gH6sCS;MahmbTG*Tbt>z&LSR|?%!Q!vds?GKM6CBM{OGVN(xMJXx?mw1Ntbh-7C%f_)& z!mNlNUKGFR;a7;2Qc}=kG~!$jC~4^4N|lSMS7?Z025h%IQUH<1v*97<5QE~!T)cW8 zCDpQK$AU~XC;ZK{vXfHvd`Vr@7cRHh=V)A6oOqRvRU)P_Szl^)xgpnla|)GRYFyjm zys9K>6ZUn@WeQQ6Mly}thP~>BhY}H0QRG~UcMIg?72QWP5a!KK3r|tbJ6N?QSR>xu_t#OG3g-#4CL?GKli=(DEr(v;>uaU&i6Y2a8L1(oIx_}_6?)1 z*2^?kRSKX6W_Vb>1gUN3=p&L#q;F&9xIFEnK&!Mst!Sk9Jh!G#=~c zxi>dIbHt;^<$An4i#G1O2hL%n5Bo zI&B&Bf$OW-DPS$CU%pZ!q5MDII!!2G(Y})R3OA=bit;@go>mIi#>91qBs4A#X)Gb6 zvQah1^fP)!=h^MGh@IK8Cb1Ny3#Yuc?z!`-#0xCdf1J_j)BULV*3sGCczk%5=!#5F z?bVH55FD{6cY5|~@IC7$*dLY6Y_hiMIl{89y*(RA=@`5*G3#aHs=d$#%XkO1I-MD| zj^{!KMn=TiOfey!g+YOF{N@66_0f%9A;P0xJiN#Fi724;W)C-=;FbD(;-PIKaGEDQ zr@}W1P34gT6Z#?VDY}GnKclNzkFoY%7Ht|7o%*t6IPuWGb!i`GR&2V<3BK+)N|mMu zO%iNJ1|pNyc@GuMxgaR|sjc43Vj0NwKA+t&2I%Rmb)oC$d*(@_95v#lV%yY*Ht=Pv zT`@Y{@Nmf1u~f)=5NK-~3*jshf;Zft?@GF}I^2c9Sq<-snf3(Tn<%U!9-|Wv;Y{aL zjYV(}rL-sOnB+YvetgIxkgB=u!zT1D}RxT=OR4Is$%h z*5BlDm@*s-PpaAQ1c4aI5qp~G7v73hkbORNaBCI1neK*^O0q-7LUZ28SRsqvG z^d;?JB*%VpU@X3z!?T<;GLzuKLqXvdY$C7V< zGxQ3jU+;Hc%Y@)#iP@CxDae)^gx%gv^k{%}W*o1VOShvPGgTo-*VxeNUPO(nOVELN9GM1P#)a1G3 zRPpAp3-@kT%(oJ+`)9ST%th!s!xJeU%Np-EbYfCBc1}nr9EcyBEc zk4_xDa@|Jk<${cq$=6!;O%S-dK3=(FeG@FMga0ro6hh^p7lhEo1C>0;UM_uDf-N%e zc2w=m%P_DJ_%yUF+k{7%D2r&vWsnl{p-I(PP#waRv4m}pIPC+{_>FnO-@s5Z( z;gfuxqb1$IsHg948bgdVT%jOWuf7&5+AlEosyUuI)qj0~)x6@JS1h+Khu2coiG~T7Qcc$tv8I}8hnXo%(Ijq38-^}HN zKT1&FuF^4rmO+e;Qg_ujxf3-;76J;Uf$BLL_cFFWT(580l%uQi&U*^PF??@~(mWoSzYc)<&1xQiU0HvL^MdcGX;kA-Y2&6;Wn`HtE6M3r=Iq z5ua2C-+n;#N4;?2QOQ%^j+1SDMrS>IlUv>?5SsaM4Km*RX8y;Ka%JY@0YY-}D=F}0 zWv79jrJ5AQ`st!O4$~xJV)rjo?(_;ylW91^4dNAeV;DDjl`${BryO>Okj&`&HlN{t zzfiVFnjIh@W7{_KcSrM0!vgCn*05rSpRZdPiVgVOq!@nh){oLMdsWM}%3 zF$Dc`>}x^|5}Jh<>4#5kfObUGNkA?!iesbSAPjq+dvS>XK#jiGOcX zKabb)R@Y3aY02!Dm1Bl@=j@f+&aZ2t9|Gw@?j_se;l{J1RWH z`FM!2eIfO|BNUkH0+(~L`{|qze8~^!yKanDoigc0Ax^YgJG;VCGej742nj4q4(4L! zXW4q`s{l4;)@MGI_Os0U_h^oNqU)B{2Yi;dYXTJr8t(G~NWsc+sKdGk zH1-K&70n%yR5*5*3AeN-ppnjE(O6#q2`VLw1qoN_7F6J|mc!~$)x zk2w=7&5A9;#=nboUJd#DvyaygTOe+9$-oswqF*%!!loQ;G(Z*G)c>4t-dPdvCVBqw znjH{2?>L3VRzI)4?NfW&x+ysR9ieYh7hsPs*2b*3-M7qV`0>3*)wrncv!d_lPE_>M z_@W(SBNNLA9HQ4o3SXnL3$d2@7D_@$Z@Yhzqcq*x*o1JrO{BH`HaD3`Zj;>amI~7E z=^zasPZLG%RR{v+4v3YRl*h@|+=}2JDu|xGF5!x z0~-?C&o0PNK`2PHDCm?g*cp%3(<*jPmetuslEp%hgG3+b3#R0s4C?QY`o%lvyhL(& z`0On2DzKfwpd<(xq=Lxl#iAADVfEd+7^s0a>gb2&P~I{l+WEzD!q{tBnTBrbx>|?z zt`Mr_4c^*{QVl>(bWg~BRIANOXr=Nqk|;cf5x~Y60aAEgxEf81{%vv)vo>kd{Aa^f zqf2~U!Bk0iph3Ar?Am6vJ0uZ08GY{~@0qpBc0p^H;E$igEO&Ajzp>E9Ic3)+yP_xXb?Fc{qW^Y;-JSS>Asw=O& zxYM|+U~2g`k}?EEO8$b-+{wY@S|wFvwD{MXt(~wdUP-HTWyZjlaf27Ce5#G(=%2Ip z@fsdeWy&lK|F#9?L)7&g@sBhfQEXZxG%LUEu$#4;FSg9vfz^^HUaLDj*=$tPjoEdC z5uIS`Im|e*?4DN@saL&J&gNYZQPyg8(I5>V*aF>IwXUJ%5uB~KPlymw-Q{r938tl( zoPgrTGWJ~S4;2eb79iMXStCG(wgFIoQ+(0VC_Zx+&k~!kDlfOm3!8OnMy;D|*Hzao z(THI#2Z85FDal~~?6^ROA3NM31Au5G`|9PlFtlhUkSI+s{_$HpNghhIC$4!WgFss; z&w1GP-8n;gu{aluyL4yH;^J2TvwV%v1EQ(1v}Z6IOjb&d85)Q3C z=b2X~n&7LJ%dkO$*8l)`1^OB2zVS+*Sso~Ywm0Jj=w2}{FWbr+!|x>u03^chP3N(6 z-4Gxy=@7{X^Dz1VrljD`tVM{n_TyD346nV}W#;rLIX_N*EYoWaVw$k`F z1k|S#=$-bIj>Y5oID_GAkao~|i`$>wqAlEfX>D?V%o!l_FX-sm+xUN93V_X_sXg&C zPj^}$dmk5P9_!qEQLlX3viZp$vnU1oUP3bnTzkUKBm9kiWY z0}DNQV%$PO-L3NjY1utht{?JFoby(#g^Flf*b{#5l#kE#?#*NCeMCEFZ&A<^&ai{? zZgV5HbKv#2#2bH(pq{vW4u{6@I;G%^APMmSCwS&6TkFmr(>(yw)ff+_y&0MfUfSby zV$c9J;A}L}6EDM~qRw=EK(opXXJOD@ii-1muJ0l>^*t0{CsedmMsSXfR|I&_tcYwJ zQ5uS~Ki=Z_H(f!M3YVL60YK&x&lY>Av?|9U z`}&!xOn7Mat^?#x%15g!La!Uqk+s2E*WV#-|1-EwE4@hxXKx_##&HSFu`Rt}LXX zCFZcY0Kj(@a!o(uCk+o5Au7vGejYc=i_@W7rtx9U7vqeup@|CJN8t{qvKS64-5g|h zU8aXr*h;WaMF5JCxC+{68C2?G5c*yp$0 zH@tEFpsxD#VcdY~GA^gasgQNt0*~VJ(XuYxZiQKwOqVqKSnbbP?mILVBJc3c-%nP- z!Ci-|u#?qRnd22Ipp^Yi<10tn+Wn#MlcucqY9G1%M$z3@(Blg^iO&og=t}aj>cH;3 zwL|L=rR!$kk(BLKC46}^{vuag+PQ*HvV{x6(C;1@Y4 z`B3pcZXJTf3C_dp5f_Vn&*(HY_M1bK9l2g@hqXr!bWS%8;bNT9_kk)hTB_S_#1`gy zyhEI(s@Z?|hIN(nX;Z#8HnzaEyJ=FP^m`H(6+=VUn>7&5Kl}P3FGKg0LXMTgUe-XC zaku`1!s41>&;n5=#Jk!3`{@s=2=WysXGWA_1YiLHjTv51AZ+3~#zX!GpW-r2zH6@( zq}0~KznsVl%}3$J!XUlci(2*=_W)RJ>A$Hhrby2fFk=`Yv6xMH;AmdHi;&@jiDF)U zoHG_0>hdLQJ%b6~srTpV=L9WusHCpO-+CuB1U~?F$H-{6eM*Xx1b2>fvLnXGczG54 zv;M&;(8%}?Ilhb=;LhEwO}{G=u50Mco&UXsk7x$Z9QqjBJxa>Nr8}x+cs&;VFKo8N zE2j^+SH%L2WDRNYcBvq;dU_x#wllu&wQJX8IEjnpx-GRaF;Q`c!+-4-`Qs6S zLEgx(fDMHnR3ii8eN#gB7SsPewOBFfBL zzazqNHRV722F-ypk;4ycf82Twq^NiXIm6Ym+1aUutw;CwRUI>3kRuCl9A}u?TMtU; zOw9XR`#QRr*jjXNOsS`H4aHjKSy&7wEBOlBwg+i=Xdea`Vwhdv?R`epOO;72MlDf= z41Fh3wrt~rb7;kfp6c$J_3|D~YbzJ3PEyF#N4GEkS7!Zx@Z7pZ(I}N8!y}Zg!+;#; zFoWPj-Km=oO{& zoS0v~6e%8qebzKHz(~(ov5T4!U82nFjo)!g%!Id27 zQ2DUclp=^VqtD+1zp#LkEB?y{^*W{`cICvAGok*HSmsNFbTtMgAH%PIQ;Gt#$H>s^ zTO1{zSJl_yB>Ci5|T@3O2tfW8c;`1l}3@bepkyFe}=VF~Xm?7Uy~)0BkXKqd6O#ZN*E zxYfMq4bvOCEH5okP|ylx@85mQ%Yoi-+3JoD9-y$eD3PCM)&2Aqd^+g~c<9sG{^bBG znTc>-Ygr3OfgXSYZ9ZZ!s2Z@AE5;s;u>JGfVY}YKjdhD94iGTVe8erWnS`iQ81su* zSHc=OhCg3&bGMKeZ_oe=2yuabT&BOgEEB)8Q}_#`GY^0Qu;{kcH&EYxoduYG3TRMu zYrRQRnBJ{_9?Zo%zi1kCJHPDzDkSQilFCQ@RCxn6s0odjD*%F(_A;EU`5ZFvw=J7? zGr8b(0KN#d(0X+dhVN%Tkb3{v{aKJ6h|Utk_5uwc@ZP8lat*C?vGFqWu5?84GYC3g-y+Cg8 zQ2jH`YTVNksBYc8LxSpm-nHQ8faDx@)-tqC>9WWSb>_>yQ}oLgD_hY)P(Bp!M^;kl Ke%?L37yk!#6pX9@ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx index 41893e785f5bf0..78039c753a2769 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx @@ -35,14 +35,14 @@ const renderWithSecretFields = ({ describe('EncryptedFieldsCallout', () => { const isCreateTests: Array<[number, string]> = [ - [1, 'Remember value label0. You must reenter it each time you edit the connector.'], + [1, 'Remember your label0 value. You must reenter it each time you edit the connector.'], [ 2, - 'Remember values label0 and label1. You must reenter them each time you edit the connector.', + 'Remember your label0 and label1 values. You must reenter them each time you edit the connector.', ], [ 3, - 'Remember values label0, label1, and label2. You must reenter them each time you edit the connector.', + 'Remember your label0, label1, and label2 values. You must reenter them each time you edit the connector.', ], ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx index 7bc0fdbc0703c5..35cbb473c6a3f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx @@ -104,7 +104,7 @@ const EncryptedFieldsCalloutComponent: React.FC = ( { values: { secretFieldsLabel, encryptedFieldsLength: totalSecretFields }, defaultMessage: - 'Remember value{encryptedFieldsLength, plural, one {} other {s}} {secretFieldsLabel}. You must reenter {encryptedFieldsLength, plural, one {it} other {them}} each time you edit the connector.', + 'Remember your {secretFieldsLabel} {encryptedFieldsLength, plural, one {value} other {values}}. You must reenter {encryptedFieldsLength, plural, one {it} other {them}} each time you edit the connector.', } )} dataTestSubj="create-connector-secrets-callout" diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts index 247b25d2be2ca5..4eec4061347b9c 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts @@ -57,5 +57,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const flyOutCancelButton = await testSubjects.find('euiFlyoutCloseButton'); await flyOutCancelButton.click(); }); + + it('email connector screenshots', async () => { + await pageObjects.common.navigateToApp('connectors'); + await pageObjects.header.waitUntilLoadingHasFinished(); + await actions.common.openNewConnectorForm('email'); + await testSubjects.setValue('nameInput', 'Gmail connector'); + await testSubjects.setValue('emailFromInput', 'test@gmail.com'); + await testSubjects.setValue('emailServiceSelectInput', 'gmail'); + await commonScreenshots.takeScreenshot('email-connector', screenshotDirectories); + const flyOutCancelButton = await testSubjects.find('euiFlyoutCloseButton'); + await flyOutCancelButton.click(); + }); }); } From 81b003d431bbd0d5d0bc3fdf507a99e50133a386 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 21 Apr 2023 14:19:53 -0400 Subject: [PATCH 02/65] [Controls] Use Internal User to Get Value of `allow_expensive_queries` (#155430) changes the way we access `allow_expensive_queries` to use the internal user. --- .../options_list/options_list_service.ts | 2 +- .../options_list_cluster_settings_route.ts | 20 ++++++------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/plugins/controls/public/services/options_list/options_list_service.ts b/src/plugins/controls/public/services/options_list/options_list_service.ts index 7152d190c997df..4ad0ecddb80a82 100644 --- a/src/plugins/controls/public/services/options_list/options_list_service.ts +++ b/src/plugins/controls/public/services/options_list/options_list_service.ts @@ -99,7 +99,7 @@ class OptionsListService implements ControlsOptionsListService { private cachedAllowExpensiveQueries = memoize(async () => { const { allowExpensiveQueries } = await this.http.get<{ allowExpensiveQueries: boolean; - }>('/api/kibana/controls/optionsList/getClusterSettings'); + }>('/api/kibana/controls/optionsList/getExpensiveQueriesSetting'); return allowExpensiveQueries; }); diff --git a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts index f3b4ea598b8868..c757a56950da39 100644 --- a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts +++ b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts @@ -8,18 +8,21 @@ import { getKbnServerError, reportServerError } from '@kbn/kibana-utils-plugin/server'; import { CoreSetup } from '@kbn/core/server'; -import { errors } from '@elastic/elasticsearch'; export const setupOptionsListClusterSettingsRoute = ({ http }: CoreSetup) => { const router = http.createRouter(); router.get( { - path: '/api/kibana/controls/optionsList/getClusterSettings', + path: '/api/kibana/controls/optionsList/getExpensiveQueriesSetting', validate: false, }, async (context, _, response) => { try { - const esClient = (await context.core).elasticsearch.client.asCurrentUser; + /** + * using internal user here because in many cases the logged in user will not have the monitor permission required + * to check cluster settings. This endpoint does not take a query, params, or a body, so there is no chance of leaking info. + */ + const esClient = (await context.core).elasticsearch.client.asInternalUser; const settings = await esClient.cluster.getSettings({ include_defaults: true, filter_path: '**.allow_expensive_queries', @@ -40,17 +43,6 @@ export const setupOptionsListClusterSettingsRoute = ({ http }: CoreSetup) => { }, }); } catch (e) { - if (e instanceof errors.ResponseError && e.body.error.type === 'security_exception') { - /** - * in cases where the user does not have the 'monitor' permission this check will fail. In these cases, we will - * fall back to assume that the allowExpensiveQueries setting is on, because it defaults to true. - */ - return response.ok({ - body: { - allowExpensiveQueries: true, - }, - }); - } const kbnErr = getKbnServerError(e); return reportServerError(response, kbnErr); } From da5a4b08d37017b246092f5c38bcd124493ad72c Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Fri, 21 Apr 2023 14:37:10 -0400 Subject: [PATCH 03/65] [Synthetics] adjust synthetics enablement doc links (#155340) ## Summary Adjusts content for Synthetics enablement doc links. --- .../monitors_page/management/disabled_callout.tsx | 6 +++++- .../monitors_page/management/invalid_api_key_callout.tsx | 2 +- .../synthetics_enablement/synthetics_enablement.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx index 00c6e9b5557278..0a75b9a44499bd 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx @@ -43,7 +43,11 @@ export const DisabledCallout = ({ total }: { total: number }) => { ) : (

{labels.CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} - + {labels.LEARN_MORE_LABEL}

diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx index 8361df0588c04f..70816a69c2188b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx @@ -34,7 +34,7 @@ export const InvalidApiKeyCalloutCallout = () => { {CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} {LEARN_MORE_LABEL} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx index b1efd88590588b..d6b927bbc43b3c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx @@ -91,7 +91,7 @@ export const EnablementEmptyState = () => { {labels.DOCS_LABEL} From dbb8e2ebecaa87f6799cb0d220b190d0f35d6d02 Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:04:54 -0400 Subject: [PATCH 04/65] [ResponseOps][Window Maintenance] Add the stop and archive actions to the maintenance window table (#155201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves https://github.com/elastic/kibana/issues/154814 ## Summary This pr adds the cancel/archive actions to the Maintenance Windows table, and adds an archive callout to the create form. @lcawl I think I need your help with some of the text in the modals 🙂 **Create Form:** Callout Screen Shot 2023-04-18 at 9 49 58 PM Modal Screen Shot 2023-04-18 at 9 50 05 PM **Cancel Modal:** Screen Shot 2023-04-18 at 9 52 18 PM **Cancel and Archive Modal:** Screen Shot 2023-04-18 at 9 52 56 PM **Archive Modal:** Screen Shot 2023-04-18 at 9 53 25 PM **Unarchive Modal:** Screen Shot 2023-04-18 at 9 54 16 PM ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Lisa Cawley --- .../use_archive_maintenance_window.test.tsx | 116 +++++++++ .../hooks/use_archive_maintenance_window.ts | 56 +++++ .../hooks/use_create_maintenance_window.ts | 4 +- .../hooks/use_find_maintenance_windows.ts | 7 +- ...sh_and_archive_maintenance_window.test.tsx | 110 +++++++++ ...e_finish_and_archive_maintenance_window.ts | 45 ++++ .../use_finish_maintenance_window.test.tsx | 85 +++++++ .../hooks/use_finish_maintenance_window.ts | 43 ++++ .../use_update_maintenance_window.test.tsx | 2 +- .../hooks/use_update_maintenance_window.ts | 7 +- .../create_maintenance_windows_form.tsx | 46 +++- .../maintenance_windows_list.test.tsx | 4 +- .../components/maintenance_windows_list.tsx | 96 ++++---- .../components/table_actions_popover.test.tsx | 101 ++++++++ .../components/table_actions_popover.tsx | 230 ++++++++++++++++++ .../pages/maintenance_windows/constants.ts | 8 +- .../pages/maintenance_windows/index.tsx | 10 +- .../pages/maintenance_windows/translations.ts | 96 ++++++++ .../maintenance_windows_api/archive.test.ts | 58 +++++ .../maintenance_windows_api/archive.ts | 35 +++ .../maintenance_windows_api/finish.test.ts | 54 ++++ .../maintenance_windows_api/finish.ts | 32 +++ 22 files changed, 1186 insertions(+), 59 deletions(-) create mode 100644 x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx create mode 100644 x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.ts create mode 100644 x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx create mode 100644 x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.ts create mode 100644 x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx create mode 100644 x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.ts create mode 100644 x-pack/plugins/alerting/public/pages/maintenance_windows/components/table_actions_popover.test.tsx create mode 100644 x-pack/plugins/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx create mode 100644 x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.test.ts create mode 100644 x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts create mode 100644 x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.test.ts create mode 100644 x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts diff --git a/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx new file mode 100644 index 00000000000000..e6bd2a4071b27f --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor } from '@testing-library/dom'; + +import { MaintenanceWindow } from '../pages/maintenance_windows/types'; +import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; +import { useArchiveMaintenanceWindow } from './use_archive_maintenance_window'; + +const mockAddDanger = jest.fn(); +const mockAddSuccess = jest.fn(); + +jest.mock('../utils/kibana_react', () => { + const originalModule = jest.requireActual('../utils/kibana_react'); + return { + ...originalModule, + useKibana: () => { + const { services } = originalModule.useKibana(); + return { + services: { + ...services, + notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } }, + }, + }; + }, + }; +}); +jest.mock('../services/maintenance_windows_api/archive', () => ({ + archiveMaintenanceWindow: jest.fn(), +})); + +const { archiveMaintenanceWindow } = jest.requireMock( + '../services/maintenance_windows_api/archive' +); + +const maintenanceWindow: MaintenanceWindow = { + title: 'archive', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + }, +}; + +let appMockRenderer: AppMockRenderer; + +describe('useArchiveMaintenanceWindow', () => { + beforeEach(() => { + jest.clearAllMocks(); + + appMockRenderer = createAppMockRenderer(); + archiveMaintenanceWindow.mockResolvedValue(maintenanceWindow); + }); + + it('should call onSuccess if api succeeds', async () => { + const { result } = renderHook(() => useArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate({ maintenanceWindowId: '123', archive: true }); + }); + await waitFor(() => + expect(mockAddSuccess).toBeCalledWith("Archived maintenance window 'archive'") + ); + }); + + it('should call onError if api fails', async () => { + archiveMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate({ maintenanceWindowId: '123', archive: true }); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to archive maintenance window.') + ); + }); + + it('should call onSuccess if api succeeds (unarchive)', async () => { + const { result } = renderHook(() => useArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate({ maintenanceWindowId: '123', archive: false }); + }); + await waitFor(() => + expect(mockAddSuccess).toBeCalledWith("Unarchived maintenance window 'archive'") + ); + }); + + it('should call onError if api fails (unarchive)', async () => { + archiveMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate({ maintenanceWindowId: '123', archive: false }); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to unarchive maintenance window.') + ); + }); +}); diff --git a/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.ts new file mode 100644 index 00000000000000..2bda74f83b9bfd --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useMutation } from '@tanstack/react-query'; + +import { useKibana } from '../utils/kibana_react'; +import { archiveMaintenanceWindow } from '../services/maintenance_windows_api/archive'; + +export function useArchiveMaintenanceWindow() { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const mutationFn = ({ + maintenanceWindowId, + archive, + }: { + maintenanceWindowId: string; + archive: boolean; + }) => { + return archiveMaintenanceWindow({ http, maintenanceWindowId, archive }); + }; + + return useMutation(mutationFn, { + onSuccess: (data, { archive }) => { + const archiveToast = i18n.translate('xpack.alerting.maintenanceWindowsArchiveSuccess', { + defaultMessage: "Archived maintenance window '{title}'", + values: { + title: data.title, + }, + }); + const unarchiveToast = i18n.translate('xpack.alerting.maintenanceWindowsUnarchiveSuccess', { + defaultMessage: "Unarchived maintenance window '{title}'", + values: { + title: data.title, + }, + }); + toasts.addSuccess(archive ? archiveToast : unarchiveToast); + }, + onError: (error, { archive }) => { + const archiveToast = i18n.translate('xpack.alerting.maintenanceWindowsArchiveFailure', { + defaultMessage: 'Failed to archive maintenance window.', + }); + const unarchiveToast = i18n.translate('xpack.alerting.maintenanceWindowsUnarchiveFailure', { + defaultMessage: 'Failed to unarchive maintenance window.', + }); + toasts.addDanger(archive ? archiveToast : unarchiveToast); + }, + }); +} diff --git a/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.ts index 08c01bb080055e..e710595bc61807 100644 --- a/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.ts +++ b/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.ts @@ -23,12 +23,12 @@ export function useCreateMaintenanceWindow() { }; return useMutation(mutationFn, { - onSuccess: (variables: MaintenanceWindow) => { + onSuccess: (data) => { toasts.addSuccess( i18n.translate('xpack.alerting.maintenanceWindowsCreateSuccess', { defaultMessage: "Created maintenance window '{title}'", values: { - title: variables.title, + title: data.title, }, }) ); diff --git a/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.ts b/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.ts index 6a71bd9c645185..10b7f3402aca1c 100644 --- a/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.ts +++ b/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.ts @@ -30,7 +30,11 @@ export const useFindMaintenanceWindows = () => { } }; - const { isLoading, data = [] } = useQuery({ + const { + isLoading, + data = [], + refetch, + } = useQuery({ queryKey: ['findMaintenanceWindows'], queryFn, onError: onErrorFn, @@ -42,5 +46,6 @@ export const useFindMaintenanceWindows = () => { return { maintenanceWindows: data, isLoading, + refetch, }; }; diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx new file mode 100644 index 00000000000000..b80dbbae355bcd --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor } from '@testing-library/dom'; + +import { MaintenanceWindow } from '../pages/maintenance_windows/types'; +import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; +import { useFinishAndArchiveMaintenanceWindow } from './use_finish_and_archive_maintenance_window'; + +const mockAddDanger = jest.fn(); +const mockAddSuccess = jest.fn(); + +jest.mock('../utils/kibana_react', () => { + const originalModule = jest.requireActual('../utils/kibana_react'); + return { + ...originalModule, + useKibana: () => { + const { services } = originalModule.useKibana(); + return { + services: { + ...services, + notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } }, + }, + }; + }, + }; +}); +jest.mock('../services/maintenance_windows_api/finish', () => ({ + finishMaintenanceWindow: jest.fn(), +})); +jest.mock('../services/maintenance_windows_api/archive', () => ({ + archiveMaintenanceWindow: jest.fn(), +})); + +const { finishMaintenanceWindow } = jest.requireMock('../services/maintenance_windows_api/finish'); +const { archiveMaintenanceWindow } = jest.requireMock( + '../services/maintenance_windows_api/archive' +); + +const maintenanceWindow: MaintenanceWindow = { + title: 'test', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + }, +}; + +let appMockRenderer: AppMockRenderer; + +describe('useFinishAndArchiveMaintenanceWindow', () => { + beforeEach(() => { + jest.clearAllMocks(); + + appMockRenderer = createAppMockRenderer(); + finishMaintenanceWindow.mockResolvedValue(maintenanceWindow); + archiveMaintenanceWindow.mockResolvedValue(maintenanceWindow); + }); + + it('should call onSuccess if api succeeds', async () => { + const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + await waitFor(() => + expect(mockAddSuccess).toBeCalledWith( + "Cancelled and archived running maintenance window 'test'" + ) + ); + }); + + it('should call onError if finish api fails', async () => { + finishMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to cancel and archive maintenance window.') + ); + }); + + it('should call onError if archive api fails', async () => { + archiveMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to cancel and archive maintenance window.') + ); + }); +}); diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.ts new file mode 100644 index 00000000000000..d68bf2c89e3792 --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useMutation } from '@tanstack/react-query'; + +import { useKibana } from '../utils/kibana_react'; +import { finishMaintenanceWindow } from '../services/maintenance_windows_api/finish'; +import { archiveMaintenanceWindow } from '../services/maintenance_windows_api/archive'; + +export function useFinishAndArchiveMaintenanceWindow() { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const mutationFn = async (maintenanceWindowId: string) => { + await finishMaintenanceWindow({ http, maintenanceWindowId }); + return archiveMaintenanceWindow({ http, maintenanceWindowId, archive: true }); + }; + + return useMutation(mutationFn, { + onSuccess: (data) => { + toasts.addSuccess( + i18n.translate('xpack.alerting.maintenanceWindowsFinishedAndArchiveSuccess', { + defaultMessage: "Cancelled and archived running maintenance window '{title}'", + values: { + title: data.title, + }, + }) + ); + }, + onError: () => { + toasts.addDanger( + i18n.translate('xpack.alerting.maintenanceWindowsFinishedAndArchiveFailure', { + defaultMessage: 'Failed to cancel and archive maintenance window.', + }) + ); + }, + }); +} diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx new file mode 100644 index 00000000000000..ed534cb835c8d5 --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor } from '@testing-library/dom'; + +import { MaintenanceWindow } from '../pages/maintenance_windows/types'; +import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; +import { useFinishMaintenanceWindow } from './use_finish_maintenance_window'; + +const mockAddDanger = jest.fn(); +const mockAddSuccess = jest.fn(); + +jest.mock('../utils/kibana_react', () => { + const originalModule = jest.requireActual('../utils/kibana_react'); + return { + ...originalModule, + useKibana: () => { + const { services } = originalModule.useKibana(); + return { + services: { + ...services, + notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } }, + }, + }; + }, + }; +}); +jest.mock('../services/maintenance_windows_api/finish', () => ({ + finishMaintenanceWindow: jest.fn(), +})); + +const { finishMaintenanceWindow } = jest.requireMock('../services/maintenance_windows_api/finish'); + +const maintenanceWindow: MaintenanceWindow = { + title: 'cancel', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + }, +}; + +let appMockRenderer: AppMockRenderer; + +describe('useFinishMaintenanceWindow', () => { + beforeEach(() => { + jest.clearAllMocks(); + + appMockRenderer = createAppMockRenderer(); + finishMaintenanceWindow.mockResolvedValue(maintenanceWindow); + }); + + it('should call onSuccess if api succeeds', async () => { + const { result } = renderHook(() => useFinishMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + await waitFor(() => + expect(mockAddSuccess).toBeCalledWith("Cancelled running maintenance window 'cancel'") + ); + }); + + it('should call onError if api fails', async () => { + finishMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useFinishMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to cancel maintenance window.') + ); + }); +}); diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.ts new file mode 100644 index 00000000000000..7e8aafa1793add --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useMutation } from '@tanstack/react-query'; + +import { useKibana } from '../utils/kibana_react'; +import { finishMaintenanceWindow } from '../services/maintenance_windows_api/finish'; + +export function useFinishMaintenanceWindow() { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const mutationFn = (maintenanceWindowId: string) => { + return finishMaintenanceWindow({ http, maintenanceWindowId }); + }; + + return useMutation(mutationFn, { + onSuccess: (data) => { + toasts.addSuccess( + i18n.translate('xpack.alerting.maintenanceWindowsFinishedSuccess', { + defaultMessage: "Cancelled running maintenance window '{title}'", + values: { + title: data.title, + }, + }) + ); + }, + onError: () => { + toasts.addDanger( + i18n.translate('xpack.alerting.maintenanceWindowsFinishedFailure', { + defaultMessage: 'Failed to cancel maintenance window.', + }) + ); + }, + }); +} diff --git a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx index 67545d83aba17d..897b44295d8c07 100644 --- a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx @@ -79,7 +79,7 @@ describe('useUpdateMaintenanceWindow', () => { }); await waitFor(() => - expect(mockAddDanger).toBeCalledWith("Failed to update maintenance window '123'") + expect(mockAddDanger).toBeCalledWith('Failed to update maintenance window.') ); }); }); diff --git a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.ts index de6596b1c766d3..c7dd73724b6dff 100644 --- a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.ts +++ b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.ts @@ -39,13 +39,10 @@ export function useUpdateMaintenanceWindow() { }) ); }, - onError: (error, variables) => { + onError: () => { toasts.addDanger( i18n.translate('xpack.alerting.maintenanceWindowsUpdateFailure', { - defaultMessage: "Failed to update maintenance window '{id}'", - values: { - id: variables.maintenanceWindowId, - }, + defaultMessage: 'Failed to update maintenance window.', }) ); }, diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx index b3fe479c21a882..0dda6a8890529b 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import moment from 'moment'; import { FIELD_TYPES, @@ -16,7 +16,10 @@ import { } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { + EuiButton, EuiButtonEmpty, + EuiCallOut, + EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiFormLabel, @@ -33,6 +36,7 @@ import { useCreateMaintenanceWindow } from '../../../hooks/use_create_maintenanc import { useUpdateMaintenanceWindow } from '../../../hooks/use_update_maintenance_window'; import { useUiSetting } from '../../../utils/kibana_react'; import { DatePickerRangeField } from './fields/date_picker_range_field'; +import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window'; const UseField = getUseField({ component: Field }); @@ -56,6 +60,7 @@ export const CreateMaintenanceWindowForm = React.memo { const [defaultStartDateValue] = useState(moment().toISOString()); const [defaultEndDateValue] = useState(moment().add(30, 'minutes').toISOString()); + const [isModalVisible, setIsModalVisible] = useState(false); const { defaultTimezone, isBrowser } = useDefaultTimezone(); const isEditMode = initialValue !== undefined && maintenanceWindowId !== undefined; @@ -63,6 +68,7 @@ export const CreateMaintenanceWindowForm = React.memo { @@ -109,6 +115,35 @@ export const CreateMaintenanceWindowForm = React.memo setIsModalVisible(false), []); + const showModal = useCallback(() => setIsModalVisible(true), []); + + const modal = useMemo(() => { + let m; + if (isModalVisible) { + m = ( + { + closeModal(); + archiveMaintenanceWindow( + { maintenanceWindowId: maintenanceWindowId!, archive: true }, + { onSuccess } + ); + }} + cancelButtonText={i18n.CANCEL} + confirmButtonText={i18n.ARCHIVE_TITLE} + defaultFocusedButton="confirm" + buttonColor="danger" + > +

{i18n.ARCHIVE_CALLOUT_SUBTITLE}

+
+ ); + } + return m; + }, [closeModal, archiveMaintenanceWindow, isModalVisible, maintenanceWindowId, onSuccess]); + return (
@@ -192,6 +227,15 @@ export const CreateMaintenanceWindowForm = React.memo : null} + {isEditMode ? ( + +

{i18n.ARCHIVE_SUBTITLE}

+ + {i18n.ARCHIVE} + + {modal} +
+ ) : null} { }); test('it renders', () => { - const result = appMockRenderer.render(); + const result = appMockRenderer.render( + {}} loading={false} items={items} /> + ); expect(result.getAllByTestId('list-item')).toHaveLength(items.length); diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx index 9d4fc521c3f665..705219b9baa2aa 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx @@ -5,29 +5,34 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { formatDate, EuiInMemoryTable, EuiBasicTableColumn, - EuiButton, - useEuiBackgroundColor, EuiFlexGroup, EuiFlexItem, SearchFilterConfig, + EuiBadge, + useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; import { MaintenanceWindowFindResponse, SortDirection } from '../types'; import * as i18n from '../translations'; import { useEditMaintenanceWindowsNavigation } from '../../../hooks/use_navigation'; +import { STATUS_DISPLAY, STATUS_SORT } from '../constants'; import { UpcomingEventsPopover } from './upcoming_events_popover'; -import { StatusColor, STATUS_DISPLAY, STATUS_SORT } from '../constants'; import { MaintenanceWindowStatus } from '../../../../common'; import { StatusFilter } from './status_filter'; +import { TableActionsPopover } from './table_actions_popover'; +import { useFinishMaintenanceWindow } from '../../../hooks/use_finish_maintenance_window'; +import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window'; +import { useFinishAndArchiveMaintenanceWindow } from '../../../hooks/use_finish_and_archive_maintenance_window'; interface MaintenanceWindowsListProps { loading: boolean; items: MaintenanceWindowFindResponse[]; + refreshData: () => void; } const columns: Array> = [ @@ -39,23 +44,9 @@ const columns: Array> = [ { field: 'status', name: i18n.TABLE_STATUS, - render: (status: string) => { + render: (status: MaintenanceWindowStatus) => { return ( - {}} - > - {STATUS_DISPLAY[status].label} - + {STATUS_DISPLAY[status].label} ); }, sortable: ({ status }) => STATUS_SORT[status], @@ -108,38 +99,61 @@ const search: { filters: SearchFilterConfig[] } = { }; export const MaintenanceWindowsList = React.memo( - ({ loading, items }) => { + ({ loading, items, refreshData }) => { + const { euiTheme } = useEuiTheme(); const { navigateToEditMaintenanceWindows } = useEditMaintenanceWindowsNavigation(); - const warningBackgroundColor = useEuiBackgroundColor('warning'); - const subduedBackgroundColor = useEuiBackgroundColor('subdued'); + const onEdit = useCallback( + (id) => navigateToEditMaintenanceWindows(id), + [navigateToEditMaintenanceWindows] + ); + const { mutate: finishMaintenanceWindow, isLoading: isLoadingFinish } = + useFinishMaintenanceWindow(); + const onCancel = useCallback( + (id) => finishMaintenanceWindow(id, { onSuccess: () => refreshData() }), + [finishMaintenanceWindow, refreshData] + ); + const { mutate: archiveMaintenanceWindow, isLoading: isLoadingArchive } = + useArchiveMaintenanceWindow(); + const onArchive = useCallback( + (id: string, archive: boolean) => + archiveMaintenanceWindow( + { maintenanceWindowId: id, archive }, + { onSuccess: () => refreshData() } + ), + [archiveMaintenanceWindow, refreshData] + ); + const { mutate: finishAndArchiveMaintenanceWindow, isLoading: isLoadingFinishAndArchive } = + useFinishAndArchiveMaintenanceWindow(); + const onCancelAndArchive = useCallback( + (id: string) => finishAndArchiveMaintenanceWindow(id, { onSuccess: () => refreshData() }), + [finishAndArchiveMaintenanceWindow, refreshData] + ); + const tableCss = useMemo(() => { return css` .euiTableRow { &.running { - background-color: ${warningBackgroundColor}; - } - - &.archived { - background-color: ${subduedBackgroundColor}; + background-color: ${euiTheme.colors.highlight}; } } `; - }, [warningBackgroundColor, subduedBackgroundColor]); + }, [euiTheme.colors.highlight]); const actions: Array> = [ { name: '', - actions: [ - { - name: i18n.TABLE_ACTION_EDIT, - isPrimary: true, - description: 'Edit maintenance window', - icon: 'pencil', - type: 'icon', - onClick: (mw: MaintenanceWindowFindResponse) => navigateToEditMaintenanceWindows(mw.id), - 'data-test-subj': 'action-edit', - }, - ], + render: ({ status, id }: { status: MaintenanceWindowStatus; id: string }) => { + return ( + + ); + }, }, ]; @@ -147,7 +161,7 @@ export const MaintenanceWindowsList = React.memo( { + let appMockRenderer: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); + }); + + test('it renders', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + + expect(result.getByTestId('table-actions-icon-button')).toBeInTheDocument(); + }); + + test('it shows the correct actions when a maintenance window is running', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + fireEvent.click(result.getByTestId('table-actions-icon-button')); + expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-cancel')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-cancel-and-archive')).toBeInTheDocument(); + }); + + test('it shows the correct actions when a maintenance window is upcoming', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + fireEvent.click(result.getByTestId('table-actions-icon-button')); + expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-archive')).toBeInTheDocument(); + }); + + test('it shows the correct actions when a maintenance window is finished', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + fireEvent.click(result.getByTestId('table-actions-icon-button')); + expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-archive')).toBeInTheDocument(); + }); + + test('it shows the correct actions when a maintenance window is archived', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + fireEvent.click(result.getByTestId('table-actions-icon-button')); + expect(result.getByTestId('table-actions-unarchive')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx new file mode 100644 index 00000000000000..4742ede93d53ce --- /dev/null +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { + EuiButtonIcon, + EuiConfirmModal, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, +} from '@elastic/eui'; +import * as i18n from '../translations'; +import { MaintenanceWindowStatus } from '../../../../common'; + +interface TableActionsPopoverProps { + id: string; + status: MaintenanceWindowStatus; + onEdit: (id: string) => void; + onCancel: (id: string) => void; + onArchive: (id: string, archive: boolean) => void; + onCancelAndArchive: (id: string) => void; +} +type ModalType = 'cancel' | 'cancelAndArchive' | 'archive' | 'unarchive'; +type ActionType = ModalType | 'edit'; + +export const TableActionsPopover: React.FC = React.memo( + ({ id, status, onEdit, onCancel, onArchive, onCancelAndArchive }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalType, setModalType] = useState(); + + const onButtonClick = useCallback(() => { + setIsPopoverOpen((open) => !open); + }, []); + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const closeModal = useCallback(() => setIsModalVisible(false), []); + const showModal = useCallback((type: ModalType) => { + setModalType(type); + setIsModalVisible(true); + }, []); + + const modal = useMemo(() => { + const modals = { + cancel: { + props: { + title: i18n.CANCEL_MODAL_TITLE, + onConfirm: () => { + closeModal(); + onCancel(id); + }, + cancelButtonText: i18n.CANCEL_MODAL_BUTTON, + confirmButtonText: i18n.CANCEL_MODAL_TITLE, + }, + subtitle: i18n.CANCEL_MODAL_SUBTITLE, + }, + cancelAndArchive: { + props: { + title: i18n.CANCEL_AND_ARCHIVE_MODAL_TITLE, + onConfirm: () => { + closeModal(); + onCancelAndArchive(id); + }, + cancelButtonText: i18n.CANCEL_MODAL_BUTTON, + confirmButtonText: i18n.CANCEL_AND_ARCHIVE_MODAL_TITLE, + }, + subtitle: i18n.CANCEL_AND_ARCHIVE_MODAL_SUBTITLE, + }, + archive: { + props: { + title: i18n.ARCHIVE_TITLE, + onConfirm: () => { + closeModal(); + onArchive(id, true); + }, + cancelButtonText: i18n.CANCEL, + confirmButtonText: i18n.ARCHIVE_TITLE, + }, + subtitle: i18n.ARCHIVE_SUBTITLE, + }, + unarchive: { + props: { + title: i18n.UNARCHIVE_MODAL_TITLE, + onConfirm: () => { + closeModal(); + onArchive(id, false); + }, + cancelButtonText: i18n.CANCEL, + confirmButtonText: i18n.UNARCHIVE_MODAL_TITLE, + }, + subtitle: i18n.UNARCHIVE_MODAL_SUBTITLE, + }, + }; + let m; + if (isModalVisible && modalType) { + const modalProps = modals[modalType]; + m = ( + +

{modalProps.subtitle}

+
+ ); + } + return m; + }, [id, modalType, isModalVisible, closeModal, onArchive, onCancel, onCancelAndArchive]); + + const items = useMemo(() => { + const menuItems = { + edit: ( + { + closePopover(); + onEdit(id); + }} + > + {i18n.TABLE_ACTION_EDIT} + + ), + cancel: ( + { + closePopover(); + showModal('cancel'); + }} + > + {i18n.TABLE_ACTION_CANCEL} + + ), + cancelAndArchive: ( + { + closePopover(); + showModal('cancelAndArchive'); + }} + > + {i18n.TABLE_ACTION_CANCEL_AND_ARCHIVE} + + ), + archive: ( + { + closePopover(); + showModal('archive'); + }} + > + {i18n.ARCHIVE} + + ), + unarchive: ( + { + closePopover(); + showModal('unarchive'); + }} + > + {i18n.TABLE_ACTION_UNARCHIVE} + + ), + }; + const statusMenuItemsMap: Record = { + running: ['edit', 'cancel', 'cancelAndArchive'], + upcoming: ['edit', 'archive'], + finished: ['edit', 'archive'], + archived: ['unarchive'], + }; + return statusMenuItemsMap[status].map((type) => menuItems[type]); + }, [id, status, onEdit, closePopover, showModal]); + + const button = useMemo( + () => ( + + ), + [onButtonClick] + ); + + return ( + <> + + + + + + + + {modal} + + ); + } +); +TableActionsPopover.displayName = 'TableActionsPopover'; diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts index 1aed4dac0568cb..27b10e804693df 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts @@ -107,15 +107,13 @@ export const RRULE_WEEKDAYS_TO_ISO_WEEKDAYS = mapValues(invert(ISO_WEEKDAYS_TO_R Number(v) ); -export const STATUS_DISPLAY: Record = { - [MaintenanceWindowStatus.Running]: { color: 'warning', label: i18n.TABLE_STATUS_RUNNING }, +export const STATUS_DISPLAY = { + [MaintenanceWindowStatus.Running]: { color: 'primary', label: i18n.TABLE_STATUS_RUNNING }, [MaintenanceWindowStatus.Upcoming]: { color: 'warning', label: i18n.TABLE_STATUS_UPCOMING }, [MaintenanceWindowStatus.Finished]: { color: 'success', label: i18n.TABLE_STATUS_FINISHED }, - [MaintenanceWindowStatus.Archived]: { color: 'text', label: i18n.TABLE_STATUS_ARCHIVED }, + [MaintenanceWindowStatus.Archived]: { color: 'default', label: i18n.TABLE_STATUS_ARCHIVED }, }; -export type StatusColor = 'warning' | 'success' | 'text'; - export const STATUS_SORT = { [MaintenanceWindowStatus.Running]: 0, [MaintenanceWindowStatus.Upcoming]: 1, diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx index ab5828f0dffa3a..fa9b54122562db 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx @@ -31,7 +31,7 @@ export const MaintenanceWindowsPage = React.memo(() => { const { docLinks } = useKibana().services; const { navigateToCreateMaintenanceWindow } = useCreateMaintenanceWindowNavigation(); - const { isLoading, maintenanceWindows } = useFindMaintenanceWindows(); + const { isLoading, maintenanceWindows, refetch } = useFindMaintenanceWindows(); useBreadcrumbs(AlertingDeepLinkId.maintenanceWindows); @@ -39,6 +39,8 @@ export const MaintenanceWindowsPage = React.memo(() => { navigateToCreateMaintenanceWindow(); }, [navigateToCreateMaintenanceWindow]); + const refreshData = useCallback(() => refetch(), [refetch]); + const showEmptyPrompt = !isLoading && maintenanceWindows.length === 0; if (isLoading) { @@ -77,7 +79,11 @@ export const MaintenanceWindowsPage = React.memo(() => { ) : ( <> - + )} diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts index 30b83963cc5b7c..65f24411a2a1a0 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts @@ -454,6 +454,102 @@ export const SAVE_MAINTENANCE_WINDOW = i18n.translate( } ); +export const TABLE_ACTION_CANCEL = i18n.translate( + 'xpack.alerting.maintenanceWindows.table.cancel', + { + defaultMessage: 'Cancel', + } +); + +export const CANCEL_MODAL_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelModal.title', + { + defaultMessage: 'Cancel maintenance window', + } +); + +export const CANCEL_MODAL_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelModal.subtitle', + { + defaultMessage: + 'Rule notifications resume immediately. Running maintenance window events are canceled; upcoming events are unaffected.', + } +); + +export const CANCEL_MODAL_BUTTON = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelModal.button', + { + defaultMessage: 'Keep running', + } +); + +export const TABLE_ACTION_CANCEL_AND_ARCHIVE = i18n.translate( + 'xpack.alerting.maintenanceWindows.table.cancelAndArchive', + { + defaultMessage: 'Cancel and archive', + } +); + +export const CANCEL_AND_ARCHIVE_MODAL_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelAndArchiveModal.title', + { + defaultMessage: 'Cancel and archive maintenance window', + } +); + +export const CANCEL_AND_ARCHIVE_MODAL_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelAndArchiveModal.subtitle', + { + defaultMessage: + 'Rule notifications resume immediately. All running and upcoming maintenance window events are canceled and the window is queued for deletion.', + } +); + +export const ARCHIVE = i18n.translate('xpack.alerting.maintenanceWindows.archive', { + defaultMessage: 'Archive', +}); + +export const ARCHIVE_TITLE = i18n.translate('xpack.alerting.maintenanceWindows.archive.title', { + defaultMessage: 'Archive maintenance window', +}); + +export const ARCHIVE_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.archive.subtitle', + { + defaultMessage: + 'Upcoming maintenance window events are canceled and the window is queued for deletion.', + } +); + +export const TABLE_ACTION_UNARCHIVE = i18n.translate( + 'xpack.alerting.maintenanceWindows.table.unarchive', + { + defaultMessage: 'Unarchive', + } +); + +export const UNARCHIVE_MODAL_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.unarchiveModal.title', + { + defaultMessage: 'Unarchive maintenance window', + } +); + +export const UNARCHIVE_MODAL_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.unarchiveModal.subtitle', + { + defaultMessage: 'Upcoming maintenance window events are scheduled.', + } +); + +export const ARCHIVE_CALLOUT_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.archiveCallout.subtitle', + { + defaultMessage: + 'The changes you have made here will not be saved. Are you sure you want to discard these unsaved changes and archive this maintenance window?', + } +); + export const EXPERIMENTAL_LABEL = i18n.translate( 'xpack.alerting.maintenanceWindows.badge.experimentalLabel', { diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.test.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.test.ts new file mode 100644 index 00000000000000..8f0e44eaf2eb99 --- /dev/null +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '@kbn/core/public/mocks'; +import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; +import { archiveMaintenanceWindow } from './archive'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('archiveMaintenanceWindow', () => { + test('should call archive maintenance window api', async () => { + const apiResponse = { + title: 'test', + duration: 1, + r_rule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + freq: 3, + interval: 1, + byweekday: ['TH'], + }, + }; + http.post.mockResolvedValueOnce(apiResponse); + + const maintenanceWindow: MaintenanceWindow = { + title: 'test', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + freq: 3, + interval: 1, + byweekday: ['TH'], + }, + }; + + const result = await archiveMaintenanceWindow({ + http, + maintenanceWindowId: '123', + archive: true, + }); + expect(result).toEqual(maintenanceWindow); + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/maintenance_window/123/_archive", + Object { + "body": "{\\"archive\\":true}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts new file mode 100644 index 00000000000000..fe07ebb04b38e9 --- /dev/null +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from '@kbn/core/public'; +import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; + +import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; + +const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ + ...rest, + rRule, +}); + +export async function archiveMaintenanceWindow({ + http, + maintenanceWindowId, + archive, +}: { + http: HttpSetup; + maintenanceWindowId: string; + archive: boolean; +}): Promise { + const res = await http.post>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/${encodeURIComponent( + maintenanceWindowId + )}/_archive`, + { body: JSON.stringify({ archive }) } + ); + + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.test.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.test.ts new file mode 100644 index 00000000000000..a67b7246a64f53 --- /dev/null +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '@kbn/core/public/mocks'; +import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; +import { finishMaintenanceWindow } from './finish'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('finishMaintenanceWindow', () => { + test('should call finish maintenance window api', async () => { + const apiResponse = { + title: 'test', + duration: 1, + r_rule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + freq: 3, + interval: 1, + byweekday: ['TH'], + }, + }; + http.post.mockResolvedValueOnce(apiResponse); + + const maintenanceWindow: MaintenanceWindow = { + title: 'test', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + freq: 3, + interval: 1, + byweekday: ['TH'], + }, + }; + + const result = await finishMaintenanceWindow({ + http, + maintenanceWindowId: '123', + }); + expect(result).toEqual(maintenanceWindow); + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/maintenance_window/123/_finish", + ] + `); + }); +}); diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts new file mode 100644 index 00000000000000..910fad0bee1c37 --- /dev/null +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from '@kbn/core/public'; +import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; + +import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; + +const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ + ...rest, + rRule, +}); + +export async function finishMaintenanceWindow({ + http, + maintenanceWindowId, +}: { + http: HttpSetup; + maintenanceWindowId: string; +}): Promise { + const res = await http.post>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/${encodeURIComponent( + maintenanceWindowId + )}/_finish` + ); + + return rewriteBodyRes(res); +} From ec6bc2db15ea0117b9ee873ad234373c11f11f41 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Fri, 21 Apr 2023 14:19:17 -0500 Subject: [PATCH 05/65] [Security Solution] add session view component to expandable flyout (#154597) --- .../alert_details_left_panel.cy.ts | 4 +- ..._details_left_panel_session_view_tab.cy.ts | 45 +++++++++++++ .../screens/document_expandable_flyout.ts | 2 + ....stories.tsx => analyze_graph.stories.tsx} | 0 .../left/components/session_view.stories.tsx | 44 +++++++++++++ .../left/components/session_view.test.tsx | 63 +++++++++++++++++++ .../flyout/left/components/session_view.tsx | 36 ++++++++++- .../public/flyout/left/components/test_ids.ts | 1 + .../flyout/left/components/translations.ts | 7 +++ .../public/flyout/left/context.tsx | 47 +++++++++++++- .../public/flyout/right/context.tsx | 3 +- 11 files changed, 243 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts rename x-pack/plugins/security_solution/public/flyout/left/components/{analyze.stories.tsx => analyze_graph.stories.tsx} (100%) create mode 100644 x-pack/plugins/security_solution/public/flyout/left/components/session_view.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/left/components/session_view.test.tsx diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel.cy.ts index a5442b786040fa..a15669a620d491 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel.cy.ts @@ -100,9 +100,7 @@ describe.skip('Alert details expandable flyout left panel', { testIsolation: fal it('should display content when switching buttons', () => { openVisualizeTab(); openSessionView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT) - .should('be.visible') - .and('have.text', 'Session view'); + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT).should('be.visible'); openGraphAnalyzer(); cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT).should('be.visible'); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts new file mode 100644 index 00000000000000..1d18b33350fd42 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA } from '../../../screens/document_expandable_flyout'; +import { + expandFirstAlertExpandableFlyout, + expandDocumentDetailsExpandableFlyoutLeftSection, +} from '../../../tasks/document_expandable_flyout'; +import { cleanKibana } from '../../../tasks/common'; +import { login, visit } from '../../../tasks/login'; +import { createRule } from '../../../tasks/api_calls/rules'; +import { getNewRule } from '../../../objects/rule'; +import { ALERTS_URL } from '../../../urls/navigation'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; + +// Skipping these for now as the feature is protected behind a feature flag set to false by default +// To run the tests locally, add 'securityFlyoutEnabled' in the Cypress config.ts here https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50 +describe.skip( + 'Alert details expandable flyout left panel session view', + { testIsolation: false }, + () => { + before(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + }); + + it('should display session view no data message', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA) + .should('be.visible') + .and('contain.text', 'No data to render') + .and('contain.text', 'No process events found for this query'); + }); + + it('should display session view component', () => {}); + } +); diff --git a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts index 2f07705ab18d47..06916d70b652a8 100644 --- a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts +++ b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts @@ -135,6 +135,8 @@ export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON = getData ); export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT = getDataTestSubjectSelector(SESSION_VIEW_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA = + getDataTestSubjectSelector('sessionView:sessionViewProcessEventsEmpty'); export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON = getDataTestSubjectSelector(VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT = diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/analyze.stories.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/analyze_graph.stories.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/flyout/left/components/analyze.stories.tsx rename to x-pack/plugins/security_solution/public/flyout/left/components/analyze_graph.stories.tsx diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/session_view.stories.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.stories.tsx new file mode 100644 index 00000000000000..40b9b7e7a5b187 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.stories.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { SessionView } from './session_view'; +import type { LeftPanelContext } from '../context'; +import { LeftFlyoutContext } from '../context'; + +export default { + component: SessionView, + title: 'Flyout/SessionView', +}; + +// TODO to get this working, we need to spent some time getting all the foundation items for storybook +// (ReduxStoreProvider, CellActionsProvider...) similarly to how it was done for the TestProvidersComponent +// see ticket https://github.com/elastic/security-team/issues/6223 +// export const Default: Story = () => { +// const contextValue = { +// getFieldsData: () => {}, +// } as unknown as LeftPanelContext; +// +// return ( +// +// +// +// ); +// }; + +export const Error: Story = () => { + const contextValue = { + getFieldsData: () => {}, + } as unknown as LeftPanelContext; + + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/session_view.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.test.tsx new file mode 100644 index 00000000000000..d334f60ba2eb3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import type { LeftPanelContext } from '../context'; +import { LeftFlyoutContext } from '../context'; +import { TestProviders } from '../../../common/mock'; +import { SESSION_VIEW_ERROR_TEST_ID, SESSION_VIEW_TEST_ID } from './test_ids'; +import { SessionView } from './session_view'; + +jest.mock('../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: jest.fn().mockReturnValue({ + services: { + sessionView: { + getSessionView: jest.fn().mockReturnValue(
), + }, + }, + }), + }; +}); + +describe('', () => { + it('renders session view correctly', () => { + const contextValue = { + getFieldsData: () => 'id', + } as unknown as LeftPanelContext; + + const wrapper = render( + + + + + + ); + expect(wrapper.getByTestId(SESSION_VIEW_TEST_ID)).toBeInTheDocument(); + }); + + it('should render error message on null eventId', () => { + const contextValue = { + getFieldsData: () => {}, + } as unknown as LeftPanelContext; + + const wrapper = render( + + + + + + ); + expect(wrapper.getByTestId(SESSION_VIEW_ERROR_TEST_ID)).toBeInTheDocument(); + expect(wrapper.getByText('Unable to display session view')).toBeInTheDocument(); + expect(wrapper.getByText('There was an error displaying session view')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/session_view.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.tsx index e62745b905640a..d48afe6e3f7125 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/session_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.tsx @@ -7,16 +7,46 @@ import type { FC } from 'react'; import React from 'react'; -import { EuiText } from '@elastic/eui'; -import { SESSION_VIEW_TEST_ID } from './test_ids'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { getField } from '../../shared/utils'; +import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations'; +import { SESSION_VIEW_ERROR_MESSAGE } from './translations'; +import { SESSION_VIEW_ERROR_TEST_ID, SESSION_VIEW_TEST_ID } from './test_ids'; +import { useKibana } from '../../../common/lib/kibana'; +import { useLeftPanelContext } from '../context'; export const SESSION_VIEW_ID = 'session_view'; +const SESSION_ENTITY_ID = 'process.entry_leader.entity_id'; /** * Session view displayed in the document details expandable flyout left section under the Visualize tab */ export const SessionView: FC = () => { - return {'Session view'}; + const { sessionView } = useKibana().services; + const { getFieldsData } = useLeftPanelContext(); + + const sessionEntityId = getField(getFieldsData(SESSION_ENTITY_ID)); + + if (!sessionEntityId) { + return ( + {ERROR_TITLE(SESSION_VIEW_ERROR_MESSAGE)}} + body={

{ERROR_MESSAGE(SESSION_VIEW_ERROR_MESSAGE)}

} + data-test-subj={SESSION_VIEW_ERROR_TEST_ID} + /> + ); + } + + return ( +
+ {sessionView.getSessionView({ + sessionEntityId, + isFullScreen: true, + })} +
+ ); }; SessionView.displayName = 'SessionView'; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts index 8b2804fae3e9f3..40cf67fddb180a 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts @@ -9,6 +9,7 @@ export const ANALYZER_GRAPH_TEST_ID = 'securitySolutionDocumentDetailsFlyoutAnal export const ANALYZE_GRAPH_ERROR_TEST_ID = 'securitySolutionDocumentDetailsFlyoutAnalyzerGraphError'; export const SESSION_VIEW_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionView'; +export const SESSION_VIEW_ERROR_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionViewError'; export const ENTITIES_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesDetails'; export const THREAT_INTELLIGENCE_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutThreatIntelligenceDetails'; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts index 8c59c8a101fb0b..f82d34c859ddff 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts @@ -13,3 +13,10 @@ export const ANALYZER_ERROR_MESSAGE = i18n.translate( defaultMessage: 'analyzer', } ); + +export const SESSION_VIEW_ERROR_MESSAGE = i18n.translate( + 'xpack.securitySolution.flyout.sessionViewErrorTitle', + { + defaultMessage: 'session view', + } +); diff --git a/x-pack/plugins/security_solution/public/flyout/left/context.tsx b/x-pack/plugins/security_solution/public/flyout/left/context.tsx index 9b564666ea4876..bf6adb5cd6a07d 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/context.tsx @@ -6,6 +6,16 @@ */ import React, { createContext, useContext, useMemo } from 'react'; +import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { SecurityPageName } from '../../../common/constants'; +import { SourcererScopeName } from '../../common/store/sourcerer/model'; +import { useSourcererDataView } from '../../common/containers/sourcerer'; +import { useTimelineEventsDetails } from '../../timelines/containers/details'; +import { useGetFieldsData } from '../../common/hooks/use_get_fields_data'; +import { useRouteSpy } from '../../common/utils/route/use_route_spy'; +import { useSpaceId } from '../../common/hooks/use_space_id'; +import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers'; import type { LeftPanelProps } from '.'; export interface LeftPanelContext { @@ -17,6 +27,10 @@ export interface LeftPanelContext { * Name of the index used in the parent's page */ indexName: string; + /** + * Retrieves searchHit values for the provided field + */ + getFieldsData: (field: string) => unknown | unknown[]; } export const LeftFlyoutContext = createContext(undefined); @@ -29,11 +43,40 @@ export type LeftPanelProviderProps = { } & Partial; export const LeftPanelProvider = ({ id, indexName, children }: LeftPanelProviderProps) => { + const currentSpaceId = useSpaceId(); + const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : ''; + const [{ pageName }] = useRouteSpy(); + const sourcererScope = + pageName === SecurityPageName.detections + ? SourcererScopeName.detections + : SourcererScopeName.default; + const sourcererDataView = useSourcererDataView(sourcererScope); + const [loading, _, searchHit] = useTimelineEventsDetails({ + indexName: eventIndex, + eventId: id ?? '', + runtimeMappings: sourcererDataView.runtimeMappings, + skip: !id, + }); + const getFieldsData = useGetFieldsData(searchHit?.fields); + const contextValue = useMemo( - () => (id && indexName ? { eventId: id, indexName } : undefined), - [id, indexName] + () => (id && indexName ? { eventId: id, indexName, getFieldsData } : undefined), + [id, indexName, getFieldsData] ); + if (loading) { + return ( + + + + ); + } + return {children}; }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/context.tsx b/x-pack/plugins/security_solution/public/flyout/right/context.tsx index 282c224b5a15f3..2da844946cbbd2 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/context.tsx @@ -19,6 +19,7 @@ import { SecurityPageName } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import type { RightPanelProps } from '.'; +import type { GetFieldsData } from '../../common/hooks/use_get_fields_data'; import { useGetFieldsData } from '../../common/hooks/use_get_fields_data'; export interface RightPanelContext { @@ -57,7 +58,7 @@ export interface RightPanelContext { /** * Retrieves searchHit values for the provided field */ - getFieldsData: (field: string) => unknown | unknown[]; + getFieldsData: GetFieldsData; } export const RightPanelContext = createContext(undefined); From e1b3bc259aecec5aff8e69b837b5723ba215a244 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 21 Apr 2023 12:19:33 -0700 Subject: [PATCH 06/65] [DOCS] Automate email-params-test.png, add title in connector table rows (#155469) --- .../connectors/action-types/email.asciidoc | 1 + .../connectors/images/email-params-test.png | Bin 173053 -> 108808 bytes .../components/actions_connectors_list.tsx | 1 + .../stack_connectors/connector_types.ts | 53 +++++++++++++++++- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index f04888a681b6ae..7d6cc75282a2f4 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -183,6 +183,7 @@ as you're creating or editing the connector in {kib}. For example: [role="screenshot"] image::management/connectors/images/email-params-test.png[Email params test] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. Email actions have the following configuration properties. diff --git a/docs/management/connectors/images/email-params-test.png b/docs/management/connectors/images/email-params-test.png index 3745bcd3235e9319517f6190dc2bcabddab1ae0a..bc2a9706e6396dddecab3f5c80e60c59e3f91b6e 100644 GIT binary patch literal 108808 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

fj3|A}J#eWI0!XTD(c`BvtJmi0qBy-P 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

}Z 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

`?2}VWi^!PRP?03&b33z0#EH!eZd%G|t};0cxW@ z-%5Ht_fzW52j6Vox6eu4$6OJ;)_0MJN8=|Nj)lw`q0=xscw`ZXm_Y3UEiK-5q`RNm z#Q>VpB5L&8`JCy^2Rhrv$a5W@n(YE2&Q6l!gEkP?{!NPE^@@4khgv;eXVSD7hqKR- zBxkMT_LJ)cnIg-!uoAFQ27!d>NQCsrmvpI_z)Ptclq3|8>isJqmhcZ=ppXKz;8t#> zc~^waP4csbo*RuDW^Pmg=?Sj63Lbn35xF<7oz$e?*4bWjs#Th%=Z_chuh6Q|;@%|g zJm6@$|I%I!;)B$RNp(e$?r}9UVTrD9e@!QIrvXDYqxr$QBE(4AW#P+aqJ@>mVW!)S znTO!&4V}h&BvJusE=QxT+ZpiW_|cUEn|Sc^yXaXkB%@c~bKs@$isvTOZrhu7J)$yk zJ;U>d%>-h$ZQJNZUa*p}ZAct`tHY2kGi;R@e|u8w(!r>f&IpU4=vS;mB9E z(Da8sW{DloKEr%1!&XE|^|o~{{iV*0k3>h@ zmXVG#l^}~vyUwFAbIIt&9@jQ}nBGsEbWwpe1yx}82Q2o3`1o2oOXBQv1N9}n9LA~v z+Pu3u=PL(hcVyCqd($&I(xx*`cnUIsE9>AVx84a&5nZbGd*JW|D!<@AW>N88p)Jbp z-QB{eXq=*7Tvh<%e~d3vtp8LqVA_qT!XOVfZxjih8-k&uDGgsC8FO=vt63pqeRtN= zDnp${X5CMCK10#!WE57c!pyXj=gX@UU`YYqa6ZjVKjVa9KnHb{eZvvZ*DYZ*!_LI< zgY76E&!ElMm?q`f_c>2}yl_<;Od~(u9%2&k>eoDo?qg^7N^SJtOrd!+uv{;BA(;Fo@Rv1avl^$k19o9OPfA1P5s>aZ_H z>bb`sa<+s~5|8BG@NX{{1?-jw0VP4i2u#{8Jrjk^_>bAHs*4PpEr1>cXX_)iiD~cE z|8)ATYq6XnHe#qqs2)RRhiRIoMs-MByYG`9E40@rB{0WiQ9=arB4w@pqX&EdbD2!` z+o}Wt3eP-_)^_ME<8o&#>XTcB`zBP|Cn%2_4_qxh8D=Q%XrDEU3lM9{f;ra=sGfxw z!s#`~m~+h6U$J2vJ=&QmO70&3PkX z+Ye~po8W3pX68Te;>wSX;0+bq>f->E&D1hSe?y+Qt$$)zW=f%$I0-3w=|zqYhs6?4e6;|MK9G^ zsm*kaeDxcVy|*zzWI&^>N2)wQH32RCY`vkRaVcmN?DtApDcQL51EL|R5{n}8v&ZWx56fuM4IQDmxkOWE*xS^{*nr7weH+9SR9?MoRxB5wC!Yy48k z7+e+SLh4DWkka8aH1Fja>cOPhU$hArb5qsiC+zjv_BH$QMVEcaxQ-Dk?hNdrCz~2C zn4`4B!3O!~;EMd|;-V1(lPLdKqKvaLM{=J*)6&)8(^pX&AUU|T|1<4B^)Ltv3P|u8 zI>h7RWyeI!tl3-@ETBCu;4y(?Hb*swZKLm9D^eZ%srLhe%wr5XwIY^o^90#5=*G!3 zk56aX)GCabksF7qZ&icR5Kt0IOEPZt1-BOt_!{63wnWo=ZbsEN>ACj}I7+VhVU9^I zm$i`P!4xX>P0p?I$|-xt6t8XC7673a*tfX7JRpiYx$2T>)wY5XvIclPw8gAU?=p7h zQs!;BKB=h}_B^{K31j5v5FhD{GlrI1DZTLgUd{-`SWa*26K~rZB|(n#e`P>tws_I8 z3#H8Ld{C!bt3POTzBc4jxe@Aeb1!$?RW{F!_NK3;=|oQ0are%thcB*RZRO*`1qFG# z-k2RFvQ^*NAy{MXp&YTe?Tl4yx_z=HHsSG!^6Qf^l~eukrx^L)3z8xSuS5-kqNJ0Fo_N z`_{#!(38tlnav*YCTU-CDX#a%0d_KWXR~8^b?Nrq+~G5t<(t>%p7;6?zI%UBrN*qr z+fj=>F9i1=HHn4V^wyRx=kY3!2SoN)64OqgHF2A!^4&o{;Yb{WzQ4Z}eYns+1+_G1ri}oqok>mi3Bd#U0$gfu0IQC`0x-K`hFy+^uR$cHpUu zOA9V^9YCaUM7MeH+$;563h{Mv^jEdj$=z}FtR9;7S@x}O8krIzPp6lp;Cru45o$9JEPS6PQE(M|E(xZK|9N3sB;0<96|9qq>+gWvWQ`?Ao6(FeT! zI(odb)X)>kA;3B7>RU+9+~a!bP9rOL%&i`HLfM|9_2Ni1t*KVIYIbf^FEqq?VO{`z zkJEfGDjU_GW$;UIty8$AAX31j#n%gq&5;f&<70=BcSIM|?9O%<#_35Q=_-gmt%_{FTnpKNA+g%L7qx0Wh6@N@cXuh7=52u zJBr=uZT#)tIt<*i?JHMNt^KeHE$(G`n1YD)ChgN#cTK$mkQCcF)|NOJPt#pMa=k>B z&h+iK(_SW`6^2>b=6W>{uJJM^cZOm4)OQ4dZe#g&;l^rMnPYc@S$?Ep8^N`}0p%3Kv|brj)_uWvynkaUyN#vSY4leVvem zSH7XsRyV64ZgBaD-YC$zt5uf`^9S^xZ4t)h6CyUNPf%ydV-#)8CNMFZAML{O+gm2{J{c#Qy!xHCaMjMXx)Rv06ef4HB-sp3mqv1%zxdw1c`AhqIn|6P{IY0*)}5>Z@W= z)W-}{MPjaB=aR8&cEa3GOKllUaEzQ7kft5=dKZ_gd@=jgvZw(};;b1BrjrWp_c!Xn zmV;3|ovkx}H|S+f${*0&KkrkPC=*mqO4ab{EC;G)4{nA%?$Nj@mkxdBQnD6c17ihK zYQpKkK7|U~(N$4dRPk7z*?sTOfw6sf>}8fjf>p!c3*CXgluRaAF<%~9hhK%PPJxGr zV3U`y5P|Ns7q4dH4LWv~dI=mh#ja1bg?t&}F+pa>@mMUh=7;mo=t7V=T)FvarZ#;JPf>O@|9f@?;{ZfR%@>SWc+b6M|!GxV6 zk!}3GTs!N2qyFM}k)=(^4-=Ngd1r3}q12pFis|%81ti3IH=pRuZaRC<-{7xS`~rY2%@p#%pfwpgyThfJQRB{x@w?EcKD~mm<^8wU30!K;tkso}$vQ!L z7o+(6&Z!-3EfZ~DAOb-w=w8M9i)dV>Un@gX1~}D;faM%-EJuM0=Z+_b@LCORc_K582g%u zL^3rPL@?vCJpPU@tySS>*gx}Uc;5LXXB)U?3kS8qeS|B0sdpX{z%V!Q8GS7v!Qh)x z0;|o4k=68bT+4=0@up34>mCyx-@7{QtNHiv`P$WMWxmB5bndKw?Yb1-WRkIgpnjSy zwu!v5-%Nt-Nx&0np%K1fAGk00;0u*+p6b_pjF-#+82OHH{|;*0UG(@hgN6b~+lJ`@ zNbx25v3Az6-D(#m+ss2OArvKsC}E(Hs0I2CcbsOH#V4)fEn|x9IeKA-4*?b8>eAr-A(fowF=7wt)cT*rDpo; zR{kVkG}A!$K9=@(n=@7u&Lk8m&ZJb8$wBVXIcXv<>iV;x@>p8K>2((%OEznrV^3Wg z|B}%OVg&-HZx(aF{mCe^GI6xLy;qVe3>t*CZOFT&kVKC$?17;rY!sI3Qs?mdmq+AO z>&03khv-MU!y4au&IEZbfp~I_aW>4hzc?*`Ij+>8_&$N!p2*Ql%GZajlzMir2kSB= zg#ru)=3{)QxC6AcYC0M%CO)ugzZ7NwOd-zZM;;qS5DWAhunPZs1Bq~rkr)mc`j?(=7cQ`@NGX9OYZsKVMvvBA1+>n3CV``~a7y$yv}Xqz zG`W!4HZLvFJ6Hv>yL8bX#zM7buH&FCL023H(%<2vcZ6qo%;vh=m4=W-gUbu_EUHEa zSeV>I(A=0xGWsJ}2XuDsoQ_5rNb}&02J)oT*4X2H=e@em6)Cg`^>fB0TRQ5&+kAIeTKS18ks8{2@%9lzg?FQRU zb{B~P!7CZ2WT&LJ!kF)7AiMWPng_CXlHR@jAOAvJw<8xDobBL#3c%<_m+ND@e&=N^HT#g7ekDieZ=G zq`y?I9JFS=wGRZM5@sGP(jTN8FR?8*NSKea&v`SlR z-gK=9UWLB@=J&nH%8>9+7WsFhsuz%fR|>DOygvarWgy2hE+G&dptmwwEmofVq(lKa zj$H$T9dEhk?hmK{w)4ddxRBV}1Q;!pE~@pHjDL782%fqP2hKpnp8=+yNR5QF{|E)3iq> zVK97A6{x^R1&uW#BB0#cUj_X-ZHtm_Yehi`ow2cIXd?`pFSUqIB(6lh;Z0@}QG`hk zIg@8>16#iIXQmZJY%z52>$gKO2@{UA*!Y5QX+l#-GCxDCfYhC#80?JEZ@T!h^Hgo& zd7~h(^boZScPRzReO|I6tUUuNBup$jcwfV?ydU8HsE|B!32Rw5Kg z*H`uaMxd0Txjvv*N!9GXtfsM4k&7EKugD>aC@-OF0`!vur5haSKJf~Z4}FOLO9reL zpPu*dWT`Ok-Sy1MnV{s3r(Lss+*9FRXF)+y5+S(>kbl%sC zs>eiB3Hf=@Kep5h_|iF!-|hpz$^rhoWXs2@uY{NDNv z0VCnJ{D38<0#D_YdM}D4oKZY#@c8vy&F^QU#-e_}*B<%Gj5?iCIvy|T4aN@^`TlC| zc6(YpqzS0zlCwn>8<@J~tQKU_dXlp(kL;?eAkV5uLgOdktwYyfjtk=Rg8R?y(?!p` zaX?$2FF~iI2FN~edJ3lyRfNB9p#15@p8TAOm)y5U)A}D@``zQj(sn%u3V8nKU~>UJ zucG&6$`t?mZLAzzpmmACDL=hDNt^z(crxYR>-ptHJhn3;3P*j8A2K#*iNsIu%p^Y;$m|k7S8Rr*58g~hs=SRBN?+xP1@jYVx%`!~H=deLSwsK9rE+J1l z5&U9yQ~}fYe@>sOp30R3>36f9rGsx`{FX!dZKK)E`!|n|Wg^Ju)%T72i_dQcArU_4N|PezmKB_Vo?dH!-h0p4hb%oFDu5PkMQm->or? z|9t$t9P58>3l?ba0T$>LT^;#9KlabD^>^#xlE#CbFz+vhzyUwv!ZJdI0=ho`58tTA ADgXcg 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 a03c34c90962e9..9edde99574ca1f 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 @@ -234,6 +234,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { <> editItem(item, EditConnectorTabs.Configuration)} key={item.id} disabled={actionTypesIndex ? !actionTypesIndex[item.actionTypeId]?.enabled : true} diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts index 4eec4061347b9c..4da22a8e807a84 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts @@ -12,8 +12,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const screenshotDirectories = ['response_ops_docs', 'stack_connectors']; const pageObjects = getPageObjects(['common', 'header']); const actions = getService('actions'); - const testSubjects = getService('testSubjects'); + const browser = getService('browser'); const comboBox = getService('comboBox'); + const find = getService('find'); + const testSubjects = getService('testSubjects'); const testIndex = `test-index`; const indexDocument = `{\n` + @@ -21,13 +23,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { `"rule_name": "{{rule.name}}",\n` + `"alert_id": "{{alert.id}}",\n` + `"context_message": "{{context.message}}"\n`; + const emailConnectorName = 'my-email-connector'; describe('connector types', function () { + let emailConnectorId: string; + before(async () => { + ({ id: emailConnectorId } = await actions.api.createConnector({ + name: emailConnectorName, + config: { + service: 'other', + from: 'bob@example.com', + host: 'some.non.existent.com', + port: 25, + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + connectorTypeId: '.email', + })); + }); + beforeEach(async () => { await pageObjects.common.navigateToApp('connectors'); await pageObjects.header.waitUntilLoadingHasFinished(); }); + after(async () => { + await actions.api.deleteConnector(emailConnectorId); + }); + it('server log connector screenshots', async () => { await pageObjects.common.navigateToApp('connectors'); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -69,5 +94,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const flyOutCancelButton = await testSubjects.find('euiFlyoutCloseButton'); await flyOutCancelButton.click(); }); + + it('test email connector screenshots', async () => { + const searchBox = await find.byCssSelector('[data-test-subj="actionsList"] .euiFieldSearch'); + await searchBox.click(); + await searchBox.clearValue(); + await searchBox.type('my actionTypeId:(.email)'); + await searchBox.pressKeys(browser.keys.ENTER); + const connectorList = await testSubjects.find('actionsTable'); + const emailConnector = await connectorList.findByCssSelector( + `[title="${emailConnectorName}"]` + ); + await emailConnector.click(); + const testButton = await testSubjects.find('testConnectorTab'); + await testButton.click(); + await testSubjects.setValue('comboBoxSearchInput', 'elastic@gmail.com'); + await testSubjects.setValue('subjectInput', 'Test subject'); + await testSubjects.setValue('messageTextArea', 'Enter message text'); + /* timing issue sometimes happens with the combobox so we just try to set the subjectInput again */ + await testSubjects.setValue('subjectInput', 'Test subject'); + await commonScreenshots.takeScreenshot( + 'email-params-test', + screenshotDirectories, + 1400, + 1024 + ); + }); }); } From 22b8e5b0a613f95fc768278932c230d17c894cfc Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 21 Apr 2023 13:43:08 -0600 Subject: [PATCH 07/65] [Security Solution] Add Timeline Hover Action to Security Dashboard Calculated Metrics (#154299) Brings back the Add to timeline action on alert counts via Cell Actions Areas implemented include: - D&R Dashboard - Open alerts by rule - Hosts by alert severity - Users by alert severity - Entity Analytics Dashboard - Host Risk Scores - User Risk Scores ![123](https://user-images.githubusercontent.com/6935300/231568732-2263ac45-ba50-4aab-b3a6-224182fbf92c.jpg) --------- Crafted with friendship by Co-authored-by: Pablo Neves Machado Co-authored-by: semd --- packages/kbn-cell-actions/README.md | 2 +- .../src/__stories__/cell_actions.stories.tsx | 14 +- .../src/components/cell_actions.test.tsx | 34 ++- .../src/components/cell_actions.tsx | 9 +- .../components/extra_actions_popover.test.tsx | 31 +-- .../src/components/extra_actions_popover.tsx | 12 +- .../components/hover_actions_popover.test.tsx | 93 ++++--- .../src/components/hover_actions_popover.tsx | 12 +- .../src/components/inline_actions.test.tsx | 22 +- .../src/components/inline_actions.tsx | 8 +- packages/kbn-cell-actions/src/constants.ts | 3 +- .../cell_action/add_to_new_timeline.test.ts | 247 ++++++++++++++++++ .../cell_action/add_to_new_timeline.ts | 117 +++++++++ .../actions/add_to_timeline/constants.ts | 23 ++ .../public/actions/add_to_timeline/index.ts | 1 + .../public/actions/constants.ts | 1 + .../public/actions/register.ts | 15 +- .../security_solution/public/actions/types.ts | 31 ++- .../ml/__snapshots__/entity.test.tsx.snap | 2 +- .../public/common/components/ml/entity.tsx | 2 +- .../score/__snapshots__/score.test.tsx.snap | 4 +- .../common/components/ml/score/score.tsx | 2 +- .../common/components/tables/helpers.tsx | 2 +- .../alerts_by_type_panel/columns.tsx | 2 +- .../host_risk_score_table/columns.tsx | 2 +- .../hosts/components/hosts_table/columns.tsx | 6 +- .../components/network_dns_table/columns.tsx | 2 +- .../network_top_countries_table/columns.tsx | 2 +- .../network_top_n_flow_table/columns.tsx | 4 +- .../explore/network/pages/details/index.tsx | 2 +- .../user_risk_score_table/columns.tsx | 2 +- .../host_alerts_table/host_alerts_table.tsx | 139 ++++++++-- .../rule_alerts_table/rule_alerts_table.tsx | 29 +- .../user_alerts_table/user_alerts_table.tsx | 137 ++++++++-- .../entity_analytics/risk_score/columns.tsx | 34 ++- .../risk_score/index.test.tsx | 31 ++- .../field_renderers/field_renderers.tsx | 2 +- 37 files changed, 885 insertions(+), 196 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.test.ts create mode 100644 x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts diff --git a/packages/kbn-cell-actions/README.md b/packages/kbn-cell-actions/README.md index 7cacff6becda61..e1ae1a73a8369e 100644 --- a/packages/kbn-cell-actions/README.md +++ b/packages/kbn-cell-actions/README.md @@ -6,7 +6,7 @@ Example: ```JSX [...] - + Hover me diff --git a/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx b/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx index 657dcc93416b80..49f812cd9005a4 100644 --- a/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx +++ b/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx @@ -50,8 +50,8 @@ export const DefaultWithControls = CellActionsTemplate.bind({}); DefaultWithControls.argTypes = { mode: { - options: [CellActionsMode.HOVER, CellActionsMode.INLINE], - defaultValue: CellActionsMode.HOVER, + options: [CellActionsMode.HOVER_DOWN, CellActionsMode.INLINE], + defaultValue: CellActionsMode.HOVER_DOWN, control: { type: 'radio', }, @@ -72,8 +72,14 @@ export const CellActionInline = ({}: {}) => ( ); -export const CellActionHoverPopup = ({}: {}) => ( - +export const CellActionHoverPopoverDown = ({}: {}) => ( + + Hover me + +); + +export const CellActionHoverPopoverRight = ({}: {}) => ( + Hover me ); diff --git a/packages/kbn-cell-actions/src/components/cell_actions.test.tsx b/packages/kbn-cell-actions/src/components/cell_actions.test.tsx index ec266889fe2552..36d0482ebf84da 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.test.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.test.tsx @@ -15,6 +15,11 @@ import { CellActionsProvider } from '../context/cell_actions_context'; const TRIGGER_ID = 'test-trigger-id'; const FIELD = { name: 'name', value: '123', type: 'text' }; +jest.mock('./hover_actions_popover', () => ({ + HoverActionsPopover: jest.fn((props) => ( + {props.anchorPosition} + )), +})); describe('CellActions', () => { it('renders', async () => { const getActionsPromise = Promise.resolve([]); @@ -54,13 +59,33 @@ describe('CellActions', () => { expect(queryByTestId('inlineActions')).toBeInTheDocument(); }); - it('renders HoverActionsPopover when mode is HOVER', async () => { + it('renders HoverActionsPopover when mode is HOVER_DOWN', async () => { const getActionsPromise = Promise.resolve([]); const getActions = () => getActionsPromise; - const { queryByTestId } = render( + const { getByTestId } = render( + + + Field value + + + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(getByTestId('hoverActionsPopover')).toBeInTheDocument(); + expect(getByTestId('hoverActionsPopover')).toHaveTextContent('downCenter'); + }); + + it('renders HoverActionsPopover when mode is HOVER_RIGHT', async () => { + const getActionsPromise = Promise.resolve([]); + const getActions = () => getActionsPromise; + + const { getByTestId } = render( - + Field value @@ -70,6 +95,7 @@ describe('CellActions', () => { await getActionsPromise; }); - expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument(); + expect(getByTestId('hoverActionsPopover')).toBeInTheDocument(); + expect(getByTestId('hoverActionsPopover')).toHaveTextContent('rightCenter'); }); }); diff --git a/packages/kbn-cell-actions/src/components/cell_actions.tsx b/packages/kbn-cell-actions/src/components/cell_actions.tsx index f6d3288ed6f7d8..df6f957575c206 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.tsx @@ -36,11 +36,17 @@ export const CellActions: React.FC = ({ [field, triggerId, metadata] ); + const anchorPosition = useMemo( + () => (mode === CellActionsMode.HOVER_DOWN ? 'downCenter' : 'rightCenter'), + [mode] + ); + const dataTestSubj = `cellActions-renderContent-${field.name}`; - if (mode === CellActionsMode.HOVER) { + if (mode === CellActionsMode.HOVER_DOWN || mode === CellActionsMode.HOVER_RIGHT) { return (

= ({ {children} {}, + actions: [], + button: , +}; describe('ExtraActionsPopOver', () => { it('renders', () => { - const { queryByTestId } = render( - {}} - actions={[]} - button={} - /> - ); + const { queryByTestId } = render(); expect(queryByTestId('extraActionsPopOver')).toBeInTheDocument(); }); @@ -33,11 +33,10 @@ describe('ExtraActionsPopOver', () => { const action = { ...makeAction('test-action'), execute: executeAction }; const { getByLabelText } = render( } /> ); @@ -56,13 +55,7 @@ describe('ExtraActionsPopOverWithAnchor', () => { it('renders', () => { const { queryByTestId } = render( - {}} - actions={[]} - anchorRef={{ current: anchorElement }} - /> + ); expect(queryByTestId('extraActionsPopOverWithAnchor')).toBeInTheDocument(); @@ -74,7 +67,7 @@ describe('ExtraActionsPopOverWithAnchor', () => { const action = { ...makeAction('test-action'), execute: executeAction }; const { getByLabelText } = render( void; @@ -32,6 +33,7 @@ interface ActionsPopOverProps { } export const ExtraActionsPopOver: React.FC = ({ + anchorPosition, actions, actionContext, isOpen, @@ -43,7 +45,7 @@ export const ExtraActionsPopOver: React.FC = ({ isOpen={isOpen} closePopover={closePopOver} panelPaddingSize="xs" - anchorPosition={'downCenter'} + anchorPosition={anchorPosition} hasArrow repositionOnScroll ownFocus @@ -59,11 +61,15 @@ export const ExtraActionsPopOver: React.FC = ({ ); interface ExtraActionsPopOverWithAnchorProps - extends Pick { + extends Pick< + ActionsPopOverProps, + 'anchorPosition' | 'actionContext' | 'closePopOver' | 'isOpen' | 'actions' + > { anchorRef: React.RefObject; } export const ExtraActionsPopOverWithAnchor = ({ + anchorPosition, anchorRef, actionContext, isOpen, @@ -77,7 +83,7 @@ export const ExtraActionsPopOverWithAnchor = ({ isOpen={isOpen} closePopover={closePopOver} panelPaddingSize="xs" - anchorPosition={'downCenter'} + anchorPosition={anchorPosition} hasArrow={false} repositionOnScroll ownFocus diff --git a/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx b/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx index b30ca63e52ec07..de68ccd6fca51f 100644 --- a/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx +++ b/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx @@ -13,11 +13,17 @@ import { makeAction } from '../mocks/helpers'; import { CellActionExecutionContext } from '../types'; import { HoverActionsPopover } from './hover_actions_popover'; -describe('HoverActionsPopover', () => { - const actionContext = { +const defaultProps = { + anchorPosition: 'rightCenter' as const, + disabledActionTypes: [], + visibleCellActions: 4, + actionContext: { trigger: { id: 'triggerId' }, field: { name: 'fieldName' }, - } as CellActionExecutionContext; + } as CellActionExecutionContext, + showActionTooltips: false, +}; +describe('HoverActionsPopover', () => { const TestComponent = () => ; jest.useFakeTimers(); @@ -25,13 +31,7 @@ describe('HoverActionsPopover', () => { const getActions = () => Promise.resolve([]); const { queryByTestId } = render( - + ); expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument(); @@ -44,12 +44,7 @@ describe('HoverActionsPopover', () => { const { queryByLabelText, getByTestId } = render( - + @@ -70,12 +65,7 @@ describe('HoverActionsPopover', () => { const { queryByLabelText, getByTestId } = render( - + @@ -101,12 +91,7 @@ describe('HoverActionsPopover', () => { const { getByTestId } = render( - + @@ -127,12 +112,7 @@ describe('HoverActionsPopover', () => { const { getByTestId, getByLabelText } = render( - + @@ -162,12 +142,7 @@ describe('HoverActionsPopover', () => { const { getByTestId, queryByLabelText } = render( - + @@ -191,6 +166,44 @@ describe('HoverActionsPopover', () => { expect(queryByLabelText('test-action-2')).toBeInTheDocument(); expect(queryByLabelText('test-action-3')).toBeInTheDocument(); }); + it('does not add css positioning when anchorPosition = downCenter', async () => { + const getActionsPromise = Promise.resolve([makeAction('test-action')]); + const getActions = () => getActionsPromise; + + const { getByLabelText, getByTestId } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + expect(Object.values(getByLabelText('Actions').style).includes('margin-top')).toEqual(false); + }); + it('adds css positioning when anchorPosition = rightCenter', async () => { + const getActionsPromise = Promise.resolve([makeAction('test-action')]); + const getActions = () => getActionsPromise; + + const { getByLabelText, getByTestId } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + expect(Object.values(getByLabelText('Actions').style).includes('margin-top')).toEqual(true); + }); }); const hoverElement = async (element: Element, waitForChange: () => Promise) => { diff --git a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx index 62ea3e766eaf72..dd087aa7553076 100644 --- a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx +++ b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx @@ -36,6 +36,7 @@ const hoverContentWrapperCSS = css` const HOVER_INTENT_DELAY = 100; // ms interface Props { + anchorPosition: 'downCenter' | 'rightCenter'; children: React.ReactNode; visibleCellActions: number; actionContext: CellActionExecutionContext; @@ -44,6 +45,7 @@ interface Props { } export const HoverActionsPopover: React.FC = ({ + anchorPosition, children, visibleCellActions, actionContext, @@ -115,12 +117,17 @@ export const HoverActionsPopover: React.FC = ({ ); }, [onMouseEnter, closeExtraActions, children]); + const panelStyle = useMemo( + () => (anchorPosition === 'rightCenter' ? { marginTop: euiThemeVars.euiSizeS } : {}), + [anchorPosition] + ); + return ( <>
= ({
{ - const actionContext = { trigger: { id: 'triggerId' } } as CellActionExecutionContext; it('renders', async () => { const getActionsPromise = Promise.resolve([]); const getActions = () => getActionsPromise; const { queryByTestId } = render( - + ); @@ -47,12 +48,7 @@ describe('InlineActions', () => { const getActions = () => getActionsPromise; const { queryAllByRole } = render( - + ); diff --git a/packages/kbn-cell-actions/src/components/inline_actions.tsx b/packages/kbn-cell-actions/src/components/inline_actions.tsx index cd2cfcc88edf0c..c46fb5c57af765 100644 --- a/packages/kbn-cell-actions/src/components/inline_actions.tsx +++ b/packages/kbn-cell-actions/src/components/inline_actions.tsx @@ -17,6 +17,7 @@ import { useLoadActions } from '../hooks/use_load_actions'; interface InlineActionsProps { actionContext: CellActionExecutionContext; + anchorPosition: 'rightCenter' | 'downCenter'; showActionTooltips: boolean; visibleCellActions: number; disabledActionTypes: string[]; @@ -24,6 +25,7 @@ interface InlineActionsProps { export const InlineActions: React.FC = ({ actionContext, + anchorPosition, showActionTooltips, visibleCellActions, disabledActionTypes, @@ -47,10 +49,9 @@ export const InlineActions: React.FC = ({ data-test-subj="inlineActions" className={`inlineActions ${isPopoverOpen ? 'inlineActions-popoverOpen' : ''}`} > - {visibleActions.map((action, index) => ( - + {visibleActions.map((action) => ( + = ({ { + const addToTimelineCellActionFactory = createAddToNewTimelineCellActionFactory({ + store, + services, + }); + const addToTimelineAction = addToTimelineCellActionFactory({ id: 'testAddToTimeline', order: 1 }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return display name', () => { + expect(addToTimelineAction.getDisplayName(context)).toEqual('Investigate in timeline'); + }); + + it('should return icon type', () => { + expect(addToTimelineAction.getIconType(context)).toEqual('timeline'); + }); + + describe('isCompatible', () => { + it('should return true if everything is okay', async () => { + expect(await addToTimelineAction.isCompatible(context)).toEqual(true); + }); + it('should return false if field not allowed', async () => { + expect( + await addToTimelineAction.isCompatible({ + ...context, + field: { ...context.field, name: 'signal.reason' }, + }) + ).toEqual(false); + }); + }); + + describe('execute', () => { + it('should execute normally', async () => { + await addToTimelineAction.execute(context); + expect(mockDispatch).toHaveBeenCalledWith(defaultAddProviderAction); + expect(mockWarningToast).not.toHaveBeenCalled(); + }); + + it('should show warning if no provider added', async () => { + await addToTimelineAction.execute({ + ...context, + field: { + ...context.field, + type: GEO_FIELD_TYPE, + }, + }); + expect(mockDispatch).not.toHaveBeenCalled(); + expect(mockWarningToast).toHaveBeenCalled(); + }); + + describe('should execute correctly when negateFilters is provided', () => { + it('should not exclude if negateFilters is false', async () => { + await addToTimelineAction.execute({ + ...context, + metadata: { + negateFilters: false, + }, + }); + expect(mockDispatch).toHaveBeenCalledWith(defaultAddProviderAction); + expect(mockWarningToast).not.toHaveBeenCalled(); + }); + + it('should exclude if negateFilters is true', async () => { + await addToTimelineAction.execute({ + ...context, + metadata: { + negateFilters: true, + }, + }); + expect(mockDispatch).toHaveBeenCalledWith({ + ...defaultAddProviderAction, + payload: { + ...defaultAddProviderAction.payload, + providers: [{ ...defaultAddProviderAction.payload.providers[0], excluded: true }], + }, + }); + expect(mockWarningToast).not.toHaveBeenCalled(); + }); + }); + + it('should clear the timeline', async () => { + await addToTimelineAction.execute(context); + expect(mockDispatch.mock.calls[0][0].type).toEqual(timelineActions.createTimeline.type); + }); + + it('should add the providers to the timeline', async () => { + await addToTimelineAction.execute({ + ...context, + metadata: { + andFilters: [{ field: 'kibana.alert.severity', value: 'low' }], + }, + }); + + expect(mockDispatch).toBeCalledWith({ + ...defaultAddProviderAction, + payload: { + ...defaultAddProviderAction.payload, + providers: [ + { + ...defaultAddProviderAction.payload.providers[0], + id: 'event-field-default-timeline-1-user_name-0-the-value', + queryMatch: defaultAddProviderAction.payload.providers[0].queryMatch, + and: [ + { + enabled: true, + excluded: false, + id: 'event-field-default-timeline-1-kibana_alert_severity-0-low', + kqlQuery: '', + name: 'kibana.alert.severity', + queryMatch: { + field: 'kibana.alert.severity', + operator: ':', + value: 'low', + }, + and: [], + }, + ], + }, + ], + }, + }); + }); + }); + + describe('getToastMessage', () => { + it('handles empty input', () => { + const result = getToastMessage({ queryMatch: { value: null } } as unknown as DataProvider); + expect(result).toEqual(''); + }); + it('handles array input', () => { + const result = getToastMessage({ + queryMatch: { value: ['hello', 'world'] }, + } as unknown as DataProvider); + expect(result).toEqual('hello, world alerts'); + }); + + it('handles single filter', () => { + const result = getToastMessage({ + queryMatch: { value }, + and: [{ queryMatch: { field: 'kibana.alert.severity', value: 'critical' } }], + } as unknown as DataProvider); + expect(result).toEqual(`critical severity alerts from ${value}`); + }); + + it('handles multiple filters', () => { + const result = getToastMessage({ + queryMatch: { value }, + and: [ + { + queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' }, + }, + { + queryMatch: { field: 'kibana.alert.severity', value: 'critical' }, + }, + ], + } as unknown as DataProvider); + expect(result).toEqual(`open, critical severity alerts from ${value}`); + }); + + it('ignores unrelated filters', () => { + const result = getToastMessage({ + queryMatch: { value }, + and: [ + { + queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' }, + }, + { + queryMatch: { field: 'kibana.alert.severity', value: 'critical' }, + }, + // currently only supporting the above fields + { + queryMatch: { field: 'user.name', value: 'something' }, + }, + ], + } as unknown as DataProvider); + expect(result).toEqual(`open, critical severity alerts from ${value}`); + }); + + it('returns entity only when unrelated filters are passed', () => { + const result = getToastMessage({ + queryMatch: { value }, + and: [{ queryMatch: { field: 'user.name', value: 'something' } }], + } as unknown as DataProvider); + expect(result).toEqual(`${value} alerts`); + }); + + it('returns entity only when no filters are passed', () => { + const result = getToastMessage({ queryMatch: { value }, and: [] } as unknown as DataProvider); + expect(result).toEqual(`${value} alerts`); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts new file mode 100644 index 00000000000000..886162803bbf15 --- /dev/null +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createCellActionFactory, type CellActionTemplate } from '@kbn/cell-actions'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { addProvider } from '../../../timelines/store/timeline/actions'; +import type { DataProvider } from '../../../../common/types'; +import { TimelineId } from '../../../../common/types'; +import type { SecurityAppStore } from '../../../common/store'; +import { fieldHasCellActions } from '../../utils'; +import { + ADD_TO_NEW_TIMELINE, + ADD_TO_TIMELINE_FAILED_TEXT, + ADD_TO_TIMELINE_FAILED_TITLE, + ADD_TO_TIMELINE_ICON, + ADD_TO_TIMELINE_SUCCESS_TITLE, + ALERTS_COUNT, + SEVERITY, +} from '../constants'; +import { createDataProviders, isValidDataProviderField } from '../data_provider'; +import { SecurityCellActionType } from '../../constants'; +import type { StartServices } from '../../../types'; +import type { SecurityCellAction } from '../../types'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; + +const severityField = 'kibana.alert.severity'; +const statusField = 'kibana.alert.workflow_status'; + +export const getToastMessage = ({ queryMatch: { value }, and = [] }: DataProvider) => { + if (value == null) { + return ''; + } + const fieldValue = Array.isArray(value) ? value.join(', ') : value.toString(); + + const descriptors = and.reduce((msg, { queryMatch }) => { + if (Array.isArray(queryMatch.value)) { + return msg; + } + if (queryMatch.field === severityField) { + msg.push(SEVERITY(queryMatch.value.toString())); + } + if (queryMatch.field === statusField) { + msg.push(queryMatch.value.toString()); + } + return msg; + }, []); + + return ALERTS_COUNT(fieldValue, descriptors.join(', ')); +}; + +export const createAddToNewTimelineCellActionFactory = createCellActionFactory( + ({ + store, + services, + }: { + store: SecurityAppStore; + services: StartServices; + }): CellActionTemplate => { + const { notifications: notificationsService } = services; + + return { + type: SecurityCellActionType.ADD_TO_TIMELINE, + getIconType: () => ADD_TO_TIMELINE_ICON, + getDisplayName: () => ADD_TO_NEW_TIMELINE, + getDisplayNameTooltip: () => ADD_TO_NEW_TIMELINE, + isCompatible: async ({ field }) => + fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type), + execute: async ({ field, metadata }) => { + const dataProviders = + createDataProviders({ + contextId: TimelineId.active, + fieldType: field.type, + values: field.value, + field: field.name, + negate: metadata?.negateFilters === true, + }) ?? []; + + for (const andFilter of metadata?.andFilters ?? []) { + const andDataProviders = + createDataProviders({ + contextId: TimelineId.active, + field: andFilter.field, + values: andFilter.value, + }) ?? []; + if (andDataProviders) { + for (const dataProvider of dataProviders) { + dataProvider.and.push(...andDataProviders); + } + } + } + + if (dataProviders.length > 0) { + // clear timeline + store.dispatch( + timelineActions.createTimeline({ + ...timelineDefaults, + id: TimelineId.active, + }) + ); + store.dispatch(addProvider({ id: TimelineId.active, providers: dataProviders })); + notificationsService.toasts.addSuccess({ + title: ADD_TO_TIMELINE_SUCCESS_TITLE(getToastMessage(dataProviders[0])), + }); + } else { + notificationsService.toasts.addWarning({ + title: ADD_TO_TIMELINE_FAILED_TITLE, + text: ADD_TO_TIMELINE_FAILED_TEXT, + }); + } + }, + }; + } +); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts index c122d7e312e4da..0396cad1103671 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts @@ -15,6 +15,29 @@ export const ADD_TO_TIMELINE = i18n.translate( defaultMessage: 'Add to timeline', } ); +export const ADD_TO_NEW_TIMELINE = i18n.translate( + 'xpack.securitySolution.actions.cellValue.addToNewTimeline.displayName', + { + defaultMessage: 'Investigate in timeline', + } +); + +export const SEVERITY = (level: string) => + i18n.translate('xpack.securitySolution.actions.addToTimeline.severityLevel', { + values: { level }, + defaultMessage: `{level} severity`, + }); + +export const ALERTS_COUNT = (entity: string, description: string) => + description !== '' + ? i18n.translate('xpack.securitySolution.actions.addToTimeline.descriptiveAlertsCountMessage', { + values: { description, entity }, + defaultMessage: '{description} alerts from {entity}', + }) + : i18n.translate('xpack.securitySolution.actions.addToTimeline.alertsCountMessage', { + values: { entity }, + defaultMessage: '{entity} alerts', + }); export const ADD_TO_TIMELINE_SUCCESS_TITLE = (value: string) => i18n.translate('xpack.securitySolution.actions.addToTimeline.addedFieldMessage', { diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts index c639dde1e23372..72e6eee17e4d49 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts @@ -6,4 +6,5 @@ */ export { createAddToTimelineCellActionFactory } from './cell_action/add_to_timeline'; +export { createAddToNewTimelineCellActionFactory } from './cell_action/add_to_new_timeline'; export { createAddToTimelineLensAction } from './lens/add_to_timeline'; diff --git a/x-pack/plugins/security_solution/public/actions/constants.ts b/x-pack/plugins/security_solution/public/actions/constants.ts index 94c2601222847b..eff71171fbcea9 100644 --- a/x-pack/plugins/security_solution/public/actions/constants.ts +++ b/x-pack/plugins/security_solution/public/actions/constants.ts @@ -7,6 +7,7 @@ export enum SecurityCellActionsTrigger { DEFAULT = 'security-default-cellActions', DETAILS_FLYOUT = 'security-detailsFlyout-cellActions', + ALERTS_COUNT = 'security-alertsCount-cellActions', } export enum SecurityCellActionType { diff --git a/x-pack/plugins/security_solution/public/actions/register.ts b/x-pack/plugins/security_solution/public/actions/register.ts index 2df581342f614e..bfea8d27163c46 100644 --- a/x-pack/plugins/security_solution/public/actions/register.ts +++ b/x-pack/plugins/security_solution/public/actions/register.ts @@ -13,6 +13,7 @@ import { createFilterInCellActionFactory, createFilterOutCellActionFactory } fro import { createAddToTimelineLensAction, createAddToTimelineCellActionFactory, + createAddToNewTimelineCellActionFactory, } from './add_to_timeline'; import { createShowTopNCellActionFactory } from './show_top_n'; import { @@ -52,6 +53,7 @@ const registerCellActions = ( filterIn: createFilterInCellActionFactory({ store, services }), filterOut: createFilterOutCellActionFactory({ store, services }), addToTimeline: createAddToTimelineCellActionFactory({ store, services }), + addToNewTimeline: createAddToNewTimelineCellActionFactory({ store, services }), showTopN: createShowTopNCellActionFactory({ store, history, services }), copyToClipboard: createCopyToClipboardCellActionFactory({ services }), toggleColumn: createToggleColumnCellActionFactory({ store }), @@ -77,6 +79,13 @@ const registerCellActions = ( ], services, }); + + registerCellActionsTrigger({ + triggerId: SecurityCellActionsTrigger.ALERTS_COUNT, + cellActions, + actionsOrder: ['addToNewTimeline'], + services, + }); }; const registerCellActionsTrigger = ({ @@ -95,8 +104,10 @@ const registerCellActionsTrigger = ({ actionsOrder.forEach((actionName, order) => { const actionFactory = cellActions[actionName]; - const action = actionFactory({ id: `${triggerId}-${actionName}`, order }); + if (actionFactory) { + const action = actionFactory({ id: `${triggerId}-${actionName}`, order }); - uiActions.addTriggerAction(triggerId, enhanceActionWithTelemetry(action, services)); + uiActions.addTriggerAction(triggerId, enhanceActionWithTelemetry(action, services)); + } }); }; diff --git a/x-pack/plugins/security_solution/public/actions/types.ts b/x-pack/plugins/security_solution/public/actions/types.ts index 1d15896ae6b355..e0cd6e764f3b81 100644 --- a/x-pack/plugins/security_solution/public/actions/types.ts +++ b/x-pack/plugins/security_solution/public/actions/types.ts @@ -6,6 +6,12 @@ */ import type { CellAction, CellActionExecutionContext, CellActionFactory } from '@kbn/cell-actions'; +import type { QueryOperator } from '../../common/types'; +export interface AndFilter { + field: string; + value: string | string[]; + operator?: QueryOperator; +} export interface SecurityMetadata extends Record { /** @@ -35,6 +41,11 @@ export interface SecurityMetadata extends Record { */ component: string; }; + /** + * `metadata.andFilters` is used by the addToTimelineAction to add + * an "and" query to the main data provider + */ + andFilters?: AndFilter[]; } export interface SecurityCellActionExecutionContext extends CellActionExecutionContext { @@ -42,13 +53,15 @@ export interface SecurityCellActionExecutionContext extends CellActionExecutionC } export type SecurityCellAction = CellAction; -// All security cell actions names -export type SecurityCellActionName = - | 'filterIn' - | 'filterOut' - | 'addToTimeline' - | 'showTopN' - | 'copyToClipboard' - | 'toggleColumn'; +export interface SecurityCellActions { + filterIn?: CellActionFactory; + filterOut?: CellActionFactory; + addToTimeline?: CellActionFactory; + addToNewTimeline?: CellActionFactory; + showTopN?: CellActionFactory; + copyToClipboard?: CellActionFactory; + toggleColumn?: CellActionFactory; +} -export type SecurityCellActions = Record; +// All security cell actions names +export type SecurityCellActionName = keyof SecurityCellActions; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap index 941078e41f917f..4cc7aad784cc84 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap @@ -10,7 +10,7 @@ exports[`entity_draggable renders correctly against snapshot 1`] = ` "value": "entity-value", } } - mode="hover" + mode="hover-down" triggerId="security-default-cellActions" visibleCellActions={5} > diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx index 00a8c0cdb3f39f..a50ea1e1f47f5e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx @@ -23,7 +23,7 @@ export const EntityComponent: React.FC = ({ entityName, entityValue }) => aggregatable: true, }} triggerId={SecurityCellActionsTrigger.DEFAULT} - mode={CellActionsMode.HOVER} + mode={CellActionsMode.HOVER_DOWN} visibleCellActions={5} > {`${entityName}: "${entityValue}"`} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap index 08e1bbe2bfc800..2d9d6a69af1f03 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap @@ -10,7 +10,7 @@ exports[`draggable_score renders correctly against snapshot 1`] = ` "value": "du", } } - mode="hover" + mode="hover-down" triggerId="security-default-cellActions" visibleCellActions={5} > @@ -28,7 +28,7 @@ exports[`draggable_score renders correctly against snapshot when the index is no "value": "du", } } - mode="hover" + mode="hover-down" triggerId="security-default-cellActions" visibleCellActions={5} > diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx index 3ad058dd2ab33a..4e5fc8b29190b7 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx @@ -26,7 +26,7 @@ export const ScoreComponent = ({ return ( 0) { return ( 0) { return ( [ return ( { title={ diff --git a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx index d8d5abb4981c0a..e6af3c9a873f70 100644 --- a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx @@ -41,7 +41,7 @@ export const getUserRiskScoreColumns = ({ return ( [ name: i18n.ALERTS_TEXT, 'data-test-subj': 'hostSeverityAlertsTable-totalAlerts', render: (totalAlerts: number, { hostName }) => ( - handleClick({ hostName })} + - - + handleClick({ hostName })} + > + + + ), }, { @@ -157,13 +176,30 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_CRITICAL_LABEL, render: (count: number, { hostName }) => ( - handleClick({ hostName, severity: 'critical' })} + - - + handleClick({ hostName, severity: 'critical' })} + > + + + ), }, @@ -172,9 +208,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_HIGH_LABEL, render: (count: number, { hostName }) => ( - handleClick({ hostName, severity: 'high' })}> - - + + handleClick({ hostName, severity: 'high' })} + > + + + ), }, @@ -183,12 +239,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_MEDIUM_LABEL, render: (count: number, { hostName }) => ( - handleClick({ hostName, severity: 'medium' })} + - - + handleClick({ hostName, severity: 'medium' })} + > + + + ), }, @@ -197,9 +270,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_LOW_LABEL, render: (count: number, { hostName }) => ( - handleClick({ hostName, severity: 'low' })}> - - + + handleClick({ hostName, severity: 'low' })} + > + + + ), }, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx index 8b0f38d0e479fa..6c60bfc727b46f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx @@ -21,6 +21,8 @@ import { import { FormattedRelative } from '@kbn/i18n-react'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; +import { CellActionsMode } from '@kbn/cell-actions'; +import { SecurityCellActionsTrigger } from '../../../../actions/constants'; import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters'; import { HeaderSection } from '../../../../common/components/header_section'; @@ -36,6 +38,7 @@ import { HoverVisibilityContainer } from '../../../../common/components/hover_vi import { BUTTON_CLASS as INSPECT_BUTTON_CLASS } from '../../../../common/components/inspect'; import { LastUpdatedAt } from '../../../../common/components/last_updated_at'; import { FormattedCount } from '../../../../common/components/formatted_number'; +import { SecurityCellActions } from '../../../../common/components/cell_actions'; export interface RuleAlertsTableProps { signalIndexName: string | null; @@ -95,13 +98,27 @@ export const getTableColumns: GetTableColumns = ({ name: i18n.RULE_ALERTS_COLUMN_ALERT_COUNT, 'data-test-subj': 'severityRuleAlertsTable-alertCount', render: (alertCount: number, { name }) => ( - openRuleInAlertsPage(name)} + - - + openRuleInAlertsPage(name)} + > + + + ), }, { diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx index ddaf75cba0f8a8..914c3c93ff240b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx @@ -20,6 +20,8 @@ import { } from '@elastic/eui'; import { ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { CellActionsMode } from '@kbn/cell-actions'; +import { SecurityCellActionsTrigger } from '../../../../actions/constants'; import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters'; import { FormattedCount } from '../../../../common/components/formatted_number'; import { HeaderSection } from '../../../../common/components/header_section'; @@ -32,6 +34,7 @@ import * as i18n from '../translations'; import { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils'; import type { UserAlertsItem } from './use_user_alerts_items'; import { useUserAlertsItems } from './use_user_alerts_items'; +import { SecurityCellActions } from '../../../../common/components/cell_actions'; interface UserAlertsTableProps { signalIndexName: string | null; @@ -142,13 +145,27 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.ALERTS_TEXT, 'data-test-subj': 'userSeverityAlertsTable-totalAlerts', render: (totalAlerts: number, { userName }) => ( - handleClick({ userName })} + - - + handleClick({ userName })} + > + + + ), }, { @@ -156,13 +173,30 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_CRITICAL_LABEL, render: (count: number, { userName }) => ( - handleClick({ userName, severity: 'critical' })} + - - + handleClick({ userName, severity: 'critical' })} + > + + + ), }, @@ -171,9 +205,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_HIGH_LABEL, render: (count: number, { userName }) => ( - handleClick({ userName, severity: 'high' })}> - - + + handleClick({ userName, severity: 'high' })} + > + + + ), }, @@ -182,12 +236,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_MEDIUM_LABEL, render: (count: number, { userName }) => ( - handleClick({ userName, severity: 'medium' })} + - - + handleClick({ userName, severity: 'medium' })} + > + + + ), }, @@ -196,9 +267,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_LOW_LABEL, render: (count: number, { userName }) => ( - handleClick({ userName, severity: 'low' })}> - - + + handleClick({ userName, severity: 'low' })} + > + + + ), }, diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx index eee34e79d99c6c..27b069c45de75d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx @@ -140,17 +140,31 @@ export const getRiskScoreColumns = ( truncateText: false, mobileOptions: { show: true }, render: (alertCount: number, risk) => ( - - openEntityOnAlertsPage( - riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name - ) - } + - - + + openEntityOnAlertsPage( + riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name + ) + } + > + + + ), }, ]; diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.test.tsx index 80a750a8bce146..7869a234ccd50f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.test.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { EntityAnalyticsRiskScores } from '.'; -import type { UserRiskScore } from '../../../../../common/search_strategy'; import { RiskScoreEntity, RiskSeverity } from '../../../../../common/search_strategy'; import type { SeverityCount } from '../../../../explore/components/risk_score/severity/types'; import { useRiskScore, useRiskScoreKpi } from '../../../../explore/containers/risk_score'; @@ -146,17 +145,17 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])( expect(queryByTestId('entity_analytics_content')).not.toBeInTheDocument(); }); - it('renders alerts count', () => { + it('renders alerts count', async () => { mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); mockUseRiskScoreKpi.mockReturnValue({ severityCount: mockSeverityCount, loading: false, }); const alertsCount = 999; - const data: UserRiskScore[] = [ + const data = [ { '@timestamp': '1234567899', - user: { + [riskEntity]: { name: 'testUsermame', risk: { rule_risks: [], @@ -176,10 +175,12 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])( ); - expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString()); + await waitFor(() => { + expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString()); + }); }); - it('navigates to alerts page with filters when alerts count is clicked', () => { + it('navigates to alerts page with filters when alerts count is clicked', async () => { mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); mockUseRiskScoreKpi.mockReturnValue({ severityCount: mockSeverityCount, @@ -211,13 +212,15 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])( fireEvent.click(getByTestId('risk-score-alerts')); - expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([ - { - title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User', - fieldName: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name', - selectedOptions: [name], - }, - ]); + await waitFor(() => { + expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([ + { + title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User', + fieldName: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name', + selectedOptions: [name], + }, + ]); + }); }); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 6238392e888bea..fdbee5da33e9f0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -317,7 +317,7 @@ export const MoreContainer = React.memo( Date: Fri, 21 Apr 2023 22:45:37 +0200 Subject: [PATCH 08/65] [Security Solution] Store expandable flyout state in the url (#154703) --- .github/CODEOWNERS | 1 + package.json | 1 + packages/kbn-expandable-flyout/index.ts | 8 +- .../kbn-expandable-flyout/src/context.tsx | 78 ++++++++++---- packages/kbn-expandable-flyout/src/reducer.ts | 3 + packages/kbn-url-state/README.md | 45 ++++++++ packages/kbn-url-state/index.test.ts | 101 ++++++++++++++++++ packages/kbn-url-state/index.ts | 9 ++ packages/kbn-url-state/jest.config.js | 13 +++ packages/kbn-url-state/kibana.jsonc | 5 + packages/kbn-url-state/package.json | 6 ++ packages/kbn-url-state/tsconfig.json | 21 ++++ packages/kbn-url-state/use_sync_to_url.ts | 87 +++++++++++++++ tsconfig.base.json | 2 + .../alert_details_url_sync.cy.ts | 55 ++++++++++ .../screens/document_expandable_flyout.ts | 3 + .../app/home/template_wrapper/index.tsx | 15 ++- .../public/common/hooks/use_url_state.ts | 3 + .../use_sync_flyout_state_with_url.test.tsx | 77 +++++++++++++ .../url/use_sync_flyout_state_with_url.tsx | 45 ++++++++ .../plugins/security_solution/tsconfig.json | 5 +- yarn.lock | 4 + 22 files changed, 563 insertions(+), 24 deletions(-) create mode 100644 packages/kbn-url-state/README.md create mode 100644 packages/kbn-url-state/index.test.ts create mode 100644 packages/kbn-url-state/index.ts create mode 100644 packages/kbn-url-state/jest.config.js create mode 100644 packages/kbn-url-state/kibana.jsonc create mode 100644 packages/kbn-url-state/package.json create mode 100644 packages/kbn-url-state/tsconfig.json create mode 100644 packages/kbn-url-state/use_sync_to_url.ts create mode 100644 x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_url_sync.cy.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e6cd6ff041cd0a..9b3a1b7a7909bc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -683,6 +683,7 @@ src/plugins/unified_search @elastic/kibana-visualizations x-pack/plugins/upgrade_assistant @elastic/platform-deployment-management x-pack/plugins/drilldowns/url_drilldown @elastic/kibana-app-services src/plugins/url_forwarding @elastic/kibana-visualizations +packages/kbn-url-state @elastic/security-threat-hunting-investigations src/plugins/usage_collection @elastic/kibana-core test/plugin_functional/plugins/usage_collection @elastic/kibana-core packages/kbn-user-profile-components @elastic/kibana-security diff --git a/package.json b/package.json index 58b87303d4a02c..b7dc6a0b647d62 100644 --- a/package.json +++ b/package.json @@ -672,6 +672,7 @@ "@kbn/upgrade-assistant-plugin": "link:x-pack/plugins/upgrade_assistant", "@kbn/url-drilldown-plugin": "link:x-pack/plugins/drilldowns/url_drilldown", "@kbn/url-forwarding-plugin": "link:src/plugins/url_forwarding", + "@kbn/url-state": "link:packages/kbn-url-state", "@kbn/usage-collection-plugin": "link:src/plugins/usage_collection", "@kbn/usage-collection-test-plugin": "link:test/plugin_functional/plugins/usage_collection", "@kbn/user-profile-components": "link:packages/kbn-user-profile-components", diff --git a/packages/kbn-expandable-flyout/index.ts b/packages/kbn-expandable-flyout/index.ts index e2ce15d85a3997..cc423eb2750904 100644 --- a/packages/kbn-expandable-flyout/index.ts +++ b/packages/kbn-expandable-flyout/index.ts @@ -7,7 +7,13 @@ */ export { ExpandableFlyout } from './src'; -export { ExpandableFlyoutProvider, useExpandableFlyoutContext } from './src/context'; +export { + ExpandableFlyoutProvider, + useExpandableFlyoutContext, + type ExpandableFlyoutContext, +} from './src/context'; + +export type { ExpandableFlyoutApi } from './src/context'; export type { ExpandableFlyoutProps } from './src'; export type { FlyoutPanel } from './src/types'; diff --git a/packages/kbn-expandable-flyout/src/context.tsx b/packages/kbn-expandable-flyout/src/context.tsx index 89e8210e9578fd..b7ad721a2b9fdf 100644 --- a/packages/kbn-expandable-flyout/src/context.tsx +++ b/packages/kbn-expandable-flyout/src/context.tsx @@ -6,7 +6,15 @@ * Side Public License, v 1. */ -import React, { createContext, useCallback, useContext, useMemo, useReducer } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useImperativeHandle, + useMemo, + useReducer, +} from 'react'; import { ActionType } from './actions'; import { reducer, State } from './reducer'; import type { FlyoutPanel } from './types'; @@ -59,19 +67,44 @@ export const ExpandableFlyoutContext = createContext & { + getState: () => State; +}; + export interface ExpandableFlyoutProviderProps { /** * React children */ children: React.ReactNode; + /** + * Triggered whenever flyout state changes. You can use it to store it's state somewhere for instance. + */ + onChanges?: (state: State) => void; + /** + * Triggered whenever flyout is closed. This is independent from the onChanges above. + */ + onClosePanels?: () => void; } /** * Wrap your plugin with this context for the ExpandableFlyout React component. */ -export const ExpandableFlyoutProvider = ({ children }: ExpandableFlyoutProviderProps) => { +export const ExpandableFlyoutProvider = React.forwardRef< + ExpandableFlyoutApi, + ExpandableFlyoutProviderProps +>(({ children, onChanges = () => {}, onClosePanels = () => {} }, ref) => { const [state, dispatch] = useReducer(reducer, initialState); + useEffect(() => { + const closed = !state.right; + if (closed) { + // manual close is singalled via separate callback + return; + } + + onChanges(state); + }, [state, onChanges]); + const openPanels = useCallback( ({ right, @@ -87,40 +120,45 @@ export const ExpandableFlyoutProvider = ({ children }: ExpandableFlyoutProviderP const openRightPanel = useCallback( (panel: FlyoutPanel) => dispatch({ type: ActionType.openRightPanel, payload: panel }), - [dispatch] + [] ); const openLeftPanel = useCallback( (panel: FlyoutPanel) => dispatch({ type: ActionType.openLeftPanel, payload: panel }), - [dispatch] + [] ); const openPreviewPanel = useCallback( (panel: FlyoutPanel) => dispatch({ type: ActionType.openPreviewPanel, payload: panel }), - [dispatch] + [] ); - const closeRightPanel = useCallback( - () => dispatch({ type: ActionType.closeRightPanel }), - [dispatch] - ); + const closeRightPanel = useCallback(() => dispatch({ type: ActionType.closeRightPanel }), []); - const closeLeftPanel = useCallback( - () => dispatch({ type: ActionType.closeLeftPanel }), - [dispatch] - ); + const closeLeftPanel = useCallback(() => dispatch({ type: ActionType.closeLeftPanel }), []); - const closePreviewPanel = useCallback( - () => dispatch({ type: ActionType.closePreviewPanel }), - [dispatch] - ); + const closePreviewPanel = useCallback(() => dispatch({ type: ActionType.closePreviewPanel }), []); const previousPreviewPanel = useCallback( () => dispatch({ type: ActionType.previousPreviewPanel }), - [dispatch] + [] ); - const closePanels = useCallback(() => dispatch({ type: ActionType.closeFlyout }), [dispatch]); + const closePanels = useCallback(() => { + dispatch({ type: ActionType.closeFlyout }); + onClosePanels(); + }, [onClosePanels]); + + useImperativeHandle( + ref, + () => { + return { + openFlyout: openPanels, + getState: () => state, + }; + }, + [openPanels, state] + ); const contextValue = useMemo( () => ({ @@ -154,7 +192,7 @@ export const ExpandableFlyoutProvider = ({ children }: ExpandableFlyoutProviderP {children} ); -}; +}); /** * Retrieve context's properties diff --git a/packages/kbn-expandable-flyout/src/reducer.ts b/packages/kbn-expandable-flyout/src/reducer.ts index 4901eccfc6bb48..bb0ff125f546ed 100644 --- a/packages/kbn-expandable-flyout/src/reducer.ts +++ b/packages/kbn-expandable-flyout/src/reducer.ts @@ -105,5 +105,8 @@ export function reducer(state: State, action: Action) { preview: [], }; } + + default: + return state; } } diff --git a/packages/kbn-url-state/README.md b/packages/kbn-url-state/README.md new file mode 100644 index 00000000000000..e7b131e3743d5c --- /dev/null +++ b/packages/kbn-url-state/README.md @@ -0,0 +1,45 @@ +# @kbn/url-state - utils for syncing state to URL + +This package provides: + +- a React hook called `useSyncToUrl` that can be used to synchronize state to the URL. This can be useful when you want to make a portion of state shareable. + +## useSyncToUrl + +The `useSyncToUrl` hook takes three arguments: + +``` +key (string): The key to use in the URL to store the state. +restore (function): A function that is called with the deserialized value from the URL. You should use this function to update your state based on the value from the URL. +cleanupOnHistoryNavigation (optional boolean, default: true): If true, the hook will clear the URL state when the user navigates using the browser's history API. +``` + +### Example usage: + +``` +import React, { useState } from 'react'; +import { useSyncToUrl } from '@kbn/url-state'; + +function MyComponent() { + const [count, setCount] = useState(0); + + useSyncToUrl('count', (value) => { + setCount(value); + }); + + const handleClick = () => { + setCount((prevCount) => prevCount + 1); + }; + + return ( +
+

Count: {count}

+ +
+ ); +} +``` + +In this example, the count state is synced to the URL using the `useSyncToUrl` hook. +Whenever the count state changes, the hook will update the URL with the new value. +When the user copies the updated url or refreshes the page, `restore` function will be called to update the count state. \ No newline at end of file diff --git a/packages/kbn-url-state/index.test.ts b/packages/kbn-url-state/index.test.ts new file mode 100644 index 00000000000000..33dc285e100e54 --- /dev/null +++ b/packages/kbn-url-state/index.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useSyncToUrl } from '.'; +import { encode } from '@kbn/rison'; + +describe('useSyncToUrl', () => { + let originalLocation: Location; + let originalHistory: History; + + beforeEach(() => { + originalLocation = window.location; + originalHistory = window.history; + delete (window as any).location; + delete (window as any).history; + + window.location = { + ...originalLocation, + search: '', + }; + window.history = { + ...originalHistory, + replaceState: jest.fn(), + }; + }); + + afterEach(() => { + window.location = originalLocation; + window.history = originalHistory; + }); + + it('should restore the value from the query string on mount', () => { + const key = 'testKey'; + const restoredValue = { test: 'value' }; + const encodedValue = encode(restoredValue); + const restore = jest.fn(); + + window.location.search = `?${key}=${encodedValue}`; + + renderHook(() => useSyncToUrl(key, restore)); + + expect(restore).toHaveBeenCalledWith(restoredValue); + }); + + it('should sync the value to the query string', () => { + const key = 'testKey'; + const valueToSerialize = { test: 'value' }; + + const { result } = renderHook(() => useSyncToUrl(key, jest.fn())); + + act(() => { + result.current(valueToSerialize); + }); + + expect(window.history.replaceState).toHaveBeenCalledWith( + { path: expect.any(String) }, + '', + '/?testKey=%28test%3Avalue%29' + ); + }); + + it('should clear the value from the query string on unmount', () => { + const key = 'testKey'; + + const { unmount } = renderHook(() => useSyncToUrl(key, jest.fn())); + + act(() => { + unmount(); + }); + + expect(window.history.replaceState).toHaveBeenCalledWith( + { path: expect.any(String) }, + '', + expect.any(String) + ); + }); + + it('should clear the value from the query string when history back or forward is pressed', () => { + const key = 'testKey'; + const restore = jest.fn(); + + renderHook(() => useSyncToUrl(key, restore, true)); + + act(() => { + window.dispatchEvent(new Event('popstate')); + }); + + expect(window.history.replaceState).toHaveBeenCalledTimes(1); + expect(window.history.replaceState).toHaveBeenCalledWith( + { path: expect.any(String) }, + '', + '/?' + ); + }); +}); diff --git a/packages/kbn-url-state/index.ts b/packages/kbn-url-state/index.ts new file mode 100644 index 00000000000000..73568222fb4c07 --- /dev/null +++ b/packages/kbn-url-state/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { useSyncToUrl } from './use_sync_to_url'; diff --git a/packages/kbn-url-state/jest.config.js b/packages/kbn-url-state/jest.config.js new file mode 100644 index 00000000000000..256a51239206c4 --- /dev/null +++ b/packages/kbn-url-state/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-url-state'], +}; diff --git a/packages/kbn-url-state/kibana.jsonc b/packages/kbn-url-state/kibana.jsonc new file mode 100644 index 00000000000000..b0ab56d6af8b76 --- /dev/null +++ b/packages/kbn-url-state/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/url-state", + "owner": "@elastic/security-threat-hunting-investigations" +} diff --git a/packages/kbn-url-state/package.json b/packages/kbn-url-state/package.json new file mode 100644 index 00000000000000..2cd753f16b872a --- /dev/null +++ b/packages/kbn-url-state/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/url-state", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-url-state/tsconfig.json b/packages/kbn-url-state/tsconfig.json new file mode 100644 index 00000000000000..3bd03b7f37b84c --- /dev/null +++ b/packages/kbn-url-state/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/rison", + ] +} diff --git a/packages/kbn-url-state/use_sync_to_url.ts b/packages/kbn-url-state/use_sync_to_url.ts new file mode 100644 index 00000000000000..e38f9bc688a8b1 --- /dev/null +++ b/packages/kbn-url-state/use_sync_to_url.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useCallback, useEffect } from 'react'; +import { encode, decode } from '@kbn/rison'; + +// https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event +const POPSTATE_EVENT = 'popstate' as const; + +/** + * Sync any object with browser query string using @knb/rison + * @param key query string param to use + * @param restore use this to handle restored state + * @param cleanupOnHistoryNavigation use history events to cleanup state on back / forward naviation. true by default + */ +export const useSyncToUrl = ( + key: string, + restore: (data: TValueToSerialize) => void, + cleanupOnHistoryNavigation = true +) => { + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const param = params.get(key); + + if (!param) { + return; + } + + const decodedQuery = decode(param); + + if (!decodedQuery) { + return; + } + + // Only restore the value if it is not falsy + restore(decodedQuery as unknown as TValueToSerialize); + }, [key, restore]); + + /** + * Synces value with the url state, under specified key. If payload is undefined, the value will be removed from the query string althogether. + */ + const syncValueToQueryString = useCallback( + (valueToSerialize?: TValueToSerialize) => { + const searchParams = new URLSearchParams(window.location.search); + + if (valueToSerialize) { + const serializedPayload = encode(valueToSerialize); + searchParams.set(key, serializedPayload); + } else { + searchParams.delete(key); + } + + const newSearch = searchParams.toString(); + + // Update query string without unnecessary re-render + const newUrl = `${window.location.pathname}?${newSearch}`; + window.history.replaceState({ path: newUrl }, '', newUrl); + }, + [key] + ); + + // Clear remove state from the url on unmount / when history back or forward is pressed + useEffect(() => { + const clearState = () => { + syncValueToQueryString(undefined); + }; + + if (cleanupOnHistoryNavigation) { + window.addEventListener(POPSTATE_EVENT, clearState); + } + + return () => { + clearState(); + + if (cleanupOnHistoryNavigation) { + window.removeEventListener(POPSTATE_EVENT, clearState); + } + }; + }, [cleanupOnHistoryNavigation, syncValueToQueryString]); + + return syncValueToQueryString; +}; diff --git a/tsconfig.base.json b/tsconfig.base.json index 6bede7391b146e..60c1f23d037b8c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1360,6 +1360,8 @@ "@kbn/url-drilldown-plugin/*": ["x-pack/plugins/drilldowns/url_drilldown/*"], "@kbn/url-forwarding-plugin": ["src/plugins/url_forwarding"], "@kbn/url-forwarding-plugin/*": ["src/plugins/url_forwarding/*"], + "@kbn/url-state": ["packages/kbn-url-state"], + "@kbn/url-state/*": ["packages/kbn-url-state/*"], "@kbn/usage-collection-plugin": ["src/plugins/usage_collection"], "@kbn/usage-collection-plugin/*": ["src/plugins/usage_collection/*"], "@kbn/usage-collection-test-plugin": ["test/plugin_functional/plugins/usage_collection"], diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_url_sync.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_url_sync.cy.ts new file mode 100644 index 00000000000000..b7580642ba5640 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_url_sync.cy.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getNewRule } from '../../../objects/rule'; +import { cleanKibana } from '../../../tasks/common'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { expandFirstAlertExpandableFlyout } from '../../../tasks/document_expandable_flyout'; +import { login, visit } from '../../../tasks/login'; +import { createRule } from '../../../tasks/api_calls/rules'; +import { ALERTS_URL } from '../../../urls/navigation'; +import { + DOCUMENT_DETAILS_FLYOUT_CLOSE_BUTTON, + DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE, +} from '../../../screens/document_expandable_flyout'; + +// Skipping these for now as the feature is protected behind a feature flag set to false by default +// To run the tests locally, add 'securityFlyoutEnabled' in the Cypress config.ts here https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50 +describe.skip('Expandable flyout state sync', { testIsolation: false }, () => { + const rule = getNewRule(); + + before(() => { + cleanKibana(); + login(); + createRule(rule); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + }); + + it('should serialize its state to url', () => { + cy.url().should('include', 'eventFlyout'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + }); + + it('should reopen the flyout after browser refresh', () => { + cy.reload(); + + cy.url().should('include', 'eventFlyout'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + }); + + it('should clear the url state when flyout is closed', () => { + cy.reload(); + + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + + cy.get(DOCUMENT_DETAILS_FLYOUT_CLOSE_BUTTON).click(); + + cy.url().should('not.include', 'eventFlyout'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts index 06916d70b652a8..5645a7de9f6ac2 100644 --- a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts +++ b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts @@ -335,3 +335,6 @@ export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ADD_TO_TIMELINE = getDataTestSubjectSelector('actionItem-security-detailsFlyout-cellActions-addToTimeline'); export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD = getDataTestSubjectSelector('actionItem-security-detailsFlyout-cellActions-copyToClipboard'); + +export const DOCUMENT_DETAILS_FLYOUT_CLOSE_BUTTON = + getDataTestSubjectSelector('euiFlyoutCloseButton'); diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index 264df10831fb72..a70a915e6aed41 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -27,6 +27,7 @@ import { useShowTimeline } from '../../../common/utils/timeline/use_show_timelin import { useShowPagesWithEmptyView } from '../../../common/utils/empty_view/use_show_pages_with_empty_view'; import { useIsPolicySettingsBarVisible } from '../../../management/pages/policy/view/policy_hooks'; import { useIsGroupedNavigationEnabled } from '../../../common/components/navigation/helpers'; +import { useSyncFlyoutStateWithUrl } from '../../../flyout/url/use_sync_flyout_state_with_url'; const NO_DATA_PAGE_MAX_WIDTH = 950; @@ -75,6 +76,8 @@ export const SecuritySolutionTemplateWrapper: React.FC + )} - {}} /> + {}} + handleOnFlyoutClosed={handleFlyoutChangedOrClosed} + /> ); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts b/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts index a5230c9ac599f0..30d914f5ccc833 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts @@ -15,6 +15,9 @@ import { useQueryTimelineByIdOnUrlChange } from './timeline/use_query_timeline_b import { useInitFlyoutFromUrlParam } from './flyout/use_init_flyout_url_param'; import { useSyncFlyoutUrlParam } from './flyout/use_sync_flyout_url_param'; +// NOTE: the expandable flyout package url state is handled here: +// x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx + export const useUrlState = () => { useSyncGlobalQueryString(); useInitSearchBarFromUrlParams(); diff --git a/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx b/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx new file mode 100644 index 00000000000000..984b2a2e223dc5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useSyncToUrl } from '@kbn/url-state'; +import { renderHook } from '@testing-library/react-hooks'; +import { useSyncFlyoutStateWithUrl } from './use_sync_flyout_state_with_url'; + +jest.mock('@kbn/url-state'); + +describe('useSyncFlyoutStateWithUrl', () => { + it('should return an array containing flyoutApi ref and handleFlyoutChanges function', () => { + const { result } = renderHook(() => useSyncFlyoutStateWithUrl()); + const [flyoutApi, handleFlyoutChanges] = result.current; + + expect(flyoutApi.current).toBeNull(); + expect(typeof handleFlyoutChanges).toBe('function'); + }); + + it('should open flyout when relevant url state is detected in the query string', () => { + jest.useFakeTimers(); + + jest.mocked(useSyncToUrl).mockImplementation((_urlKey, callback) => { + setTimeout(() => callback({ mocked: { flyout: 'state' } }), 0); + return jest.fn(); + }); + + const { result } = renderHook(() => useSyncFlyoutStateWithUrl()); + const [flyoutApi, handleFlyoutChanges] = result.current; + + const flyoutApiMock: ExpandableFlyoutApi = { + openFlyout: jest.fn(), + getState: () => ({ left: undefined, right: undefined, preview: [] }), + }; + + expect(typeof handleFlyoutChanges).toBe('function'); + expect(flyoutApi.current).toBeNull(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (flyoutApi as any).current = flyoutApiMock; + + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + + expect(flyoutApiMock.openFlyout).toHaveBeenCalledTimes(1); + expect(flyoutApiMock.openFlyout).toHaveBeenCalledWith({ mocked: { flyout: 'state' } }); + }); + + it('should sync flyout state to url whenever handleFlyoutChanges is called by the consumer', () => { + const syncStateToUrl = jest.fn(); + jest.mocked(useSyncToUrl).mockImplementation((_urlKey, callback) => { + setTimeout(() => callback({ mocked: { flyout: 'state' } }), 0); + return syncStateToUrl; + }); + + const { result } = renderHook(() => useSyncFlyoutStateWithUrl()); + const [_flyoutApi, handleFlyoutChanges] = result.current; + + handleFlyoutChanges(); + + expect(syncStateToUrl).toHaveBeenCalledTimes(1); + expect(syncStateToUrl).toHaveBeenLastCalledWith(undefined); + + handleFlyoutChanges({ left: undefined, right: undefined, preview: [] }); + + expect(syncStateToUrl).toHaveBeenLastCalledWith({ + left: undefined, + right: undefined, + preview: undefined, + }); + expect(syncStateToUrl).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx b/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx new file mode 100644 index 00000000000000..be1b28147f63d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useRef } from 'react'; +import type { ExpandableFlyoutApi, ExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { useSyncToUrl } from '@kbn/url-state'; +import last from 'lodash/last'; + +const URL_KEY = 'eventFlyout' as const; + +type FlyoutState = Parameters[0]; + +/** + * Sync flyout state with the url and open it when relevant url state is detected in the query string + * @returns [ref, flyoutChangesHandler] + */ +export const useSyncFlyoutStateWithUrl = () => { + const flyoutApi = useRef(null); + + const syncStateToUrl = useSyncToUrl(URL_KEY, (data) => { + flyoutApi.current?.openFlyout(data); + }); + + // This should be bound to flyout changed and closed events. + // When flyout is closed, url state is cleared + const handleFlyoutChanges = useCallback( + (state?: ExpandableFlyoutContext['panels']) => { + if (!state) { + return syncStateToUrl(undefined); + } + + return syncStateToUrl({ + ...state, + preview: last(state.preview), + }); + }, + [syncStateToUrl] + ); + + return [flyoutApi, handleFlyoutChanges] as const; +}; diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 622785a43b4e8d..6b14dd7b616bf3 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -23,7 +23,9 @@ ], "kbn_references": [ "@kbn/core", - { "path": "../../../src/setup_node_env/tsconfig.json" }, + { + "path": "../../../src/setup_node_env/tsconfig.json" + }, "@kbn/data-plugin", "@kbn/embeddable-plugin", "@kbn/files-plugin", @@ -153,5 +155,6 @@ "@kbn/security-solution-side-nav", "@kbn/core-lifecycle-browser", "@kbn/ecs", + "@kbn/url-state" ] } diff --git a/yarn.lock b/yarn.lock index 5ab19eee75bd9f..38b7781f7b17a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5457,6 +5457,10 @@ version "0.0.0" uid "" +"@kbn/url-state@link:packages/kbn-url-state": + version "0.0.0" + uid "" + "@kbn/usage-collection-plugin@link:src/plugins/usage_collection": version "0.0.0" uid "" From 4eeec1865f845fd7a0c07875e726b656c0a3278a Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Fri, 21 Apr 2023 15:45:55 -0500 Subject: [PATCH 09/65] [Security Solution] add threat intelligence overview to expandable flyout (#155328) --- ...ert_details_right_panel_overview_tab.cy.ts | 36 +++ .../screens/document_expandable_flyout.ts | 12 + .../right/components/insights_section.tsx | 2 + .../insights_subsection.stories.tsx | 38 +++ .../components/insights_subsection.test.tsx | 67 ++++++ .../right/components/insights_subsection.tsx | 79 +++++++ .../insights_summary_panel.stories.tsx | 156 +++++++++++++ .../insights_summary_panel.test.tsx | 90 ++++++++ .../components/insights_summary_panel.tsx | 106 +++++++++ .../flyout/right/components/test_ids.ts | 12 + .../threat_intelligence_overview.test.tsx | 195 ++++++++++++++++ .../threat_intelligence_overview.tsx | 91 ++++++++ .../flyout/right/components/translations.ts | 42 ++++ .../use_fetch_threat_intelligence.test.tsx | 216 ++++++++++++++++++ .../hooks/use_fetch_threat_intelligence.ts | 108 +++++++++ 15 files changed, 1250 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index c98c84b800079e..57ea1a2d4efdb2 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -29,6 +29,10 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON, DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON, } from '../../../screens/document_expandable_flyout'; import { expandFirstAlertExpandableFlyout, @@ -180,6 +184,38 @@ describe.skip( .click(); cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); }); + + // TODO work on getting proper IoC data to make the threat intelligence section work here + it.skip('should display threat intelligence section', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER) + .scrollIntoView() + .should('be.visible') + .and('have.text', 'Threat Intelligence'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT) + .should('be.visible') + .within(() => { + // threat match detected + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) + .eq(0) + .should('be.visible') + .and('have.text', '1 threat match detected'); // TODO + + // field with threat enrichement + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) + .eq(1) + .should('be.visible') + .and('have.text', '1 field enriched with threat intelligence'); // TODO + }); + }); + + // TODO work on getting proper IoC data to make the threat intelligence section work here + // and improve when we can navigate Threat Intelligence to sub tab directly + it.skip('should navigate to left panel, entities tab when view all fields of threat intelligence is clicked', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON) + .should('be.visible') + .click(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); + }); }); describe('visualizations section', () => { diff --git a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts index 5645a7de9f6ac2..dafd16932d1945 100644 --- a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts +++ b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts @@ -73,6 +73,10 @@ import { ENTITIES_VIEW_ALL_BUTTON_TEST_ID, VISUALIZATIONS_SECTION_HEADER_TEST_ID, ANALYZER_TREE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, } from '../../public/flyout/right/components/test_ids'; import { getClassSelector, @@ -303,6 +307,14 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER = getDataTestSubjectSelector(ENTITY_PANEL_HEADER_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT = getDataTestSubjectSelector(ENTITY_PANEL_CONTENT_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER = getDataTestSubjectSelector(VISUALIZATIONS_SECTION_HEADER_TEST_ID); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx index 8409676b610b0c..9efb979ba7a26a 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; import { INSIGHTS_TEST_ID } from './test_ids'; import { INSIGHTS_TITLE } from './translations'; import { EntitiesOverview } from './entities_overview'; @@ -25,6 +26,7 @@ export const InsightsSection: React.FC = ({ expanded = fal return ( + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx new file mode 100644 index 00000000000000..c1fc9dfe8a7f88 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { InsightsSubSection } from './insights_subsection'; + +export default { + component: InsightsSubSection, + title: 'Flyout/InsightsSubSection', +}; + +const title = 'Title'; +const children =
{'hello'}
; + +export const Basic: Story = () => { + return {children}; +}; + +export const Loading: Story = () => { + return ( + + {null} + + ); +}; + +export const NoTitle: Story = () => { + return {children}; +}; + +export const NoChildren: Story = () => { + return {null}; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx new file mode 100644 index 00000000000000..271953c8e81054 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { InsightsSubSection } from './insights_subsection'; + +const title = 'Title'; +const dataTestSubj = 'test'; +const children =
{'hello'}
; + +describe('', () => { + it('should render children component', () => { + const { getByTestId } = render( + + {children} + + ); + + const titleDataTestSubj = `${dataTestSubj}Title`; + const contentDataTestSubj = `${dataTestSubj}Content`; + + expect(getByTestId(titleDataTestSubj)).toHaveTextContent(title); + expect(getByTestId(contentDataTestSubj)).toBeInTheDocument(); + }); + + it('should render loading component', () => { + const { getByTestId } = render( + + {children} + + ); + + const loadingDataTestSubj = `${dataTestSubj}Loading`; + expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument(); + }); + + it('should render null if error', () => { + const { container } = render( + + {children} + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null if no title', () => { + const { container } = render({children}); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null if no children', () => { + const { container } = render( + + {null} + + ); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx new file mode 100644 index 00000000000000..4b5c1a541e3165 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, EuiTitle } from '@elastic/eui'; + +export interface InsightsSectionProps { + /** + * Renders a loading spinner if true + */ + loading?: boolean; + /** + * Returns a null component if true + */ + error?: boolean; + /** + * Title at the top of the component + */ + title: string; + /** + * Content of the component + */ + children: React.ReactNode; + /** + * Prefix data-test-subj to use for the elements + */ + ['data-test-subj']?: string; +} + +/** + * Presentational component to handle loading and error in the subsections of the Insights section. + * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results + */ +export const InsightsSubSection: React.FC = ({ + loading = false, + error = false, + title, + 'data-test-subj': dataTestSubj, + children, +}) => { + const loadingDataTestSubj = `${dataTestSubj}Loading`; + // showing the loading in this component instead of SummaryPanel because we're hiding the entire section if no data + + if (loading) { + return ( + + + + + + ); + } + + // hide everything + if (error || !title || !children) { + return null; + } + + const titleDataTestSubj = `${dataTestSubj}Title`; + const contentDataTestSubj = `${dataTestSubj}Content`; + + return ( + <> + +
{title}
+
+ + + {children} + + + ); +}; + +InsightsSubSection.displayName = 'InsightsSubSection'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx new file mode 100644 index 00000000000000..5637d3c0368601 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { css } from '@emotion/react'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; + +export default { + component: InsightsSummaryPanel, + title: 'Flyout/InsightsSummaryPanel', +}; + +export const Default: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + { + icon: 'warning', + value: 2, + text: 'this is test for orange', + color: 'rgb(255,126,98)', + }, + { + icon: 'warning', + value: 3, + text: 'this is test for yellow', + color: 'rgb(241,216,11)', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const InvalidColor: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for an invalid color (abc)', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const NoColor: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + }, + { + icon: 'warning', + value: 2, + text: 'this is test for orange', + }, + { + icon: 'warning', + value: 3, + text: 'this is test for yellow', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const LongText: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is an extremely long text to verify it is properly cut off and and we show three dots at the end', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; +export const LongNumber: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is an extremely long value to verify it is properly cut off and and we show three dots at the end', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const NoData: Story = () => { + const data: InsightsSummaryPanelData[] = []; + + return ( +
+ +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx new file mode 100644 index 00000000000000..9ecbbcc7fc0a57 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { + INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID, +} from './test_ids'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; + +describe('', () => { + it('should render by default', () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + ]; + + const { getByTestId } = render( + + + + ); + + const iconTestId = `${INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID}0`; + const valueTestId = `${INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID}0`; + const colorTestId = `${INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID}0`; + expect(getByTestId(iconTestId)).toBeInTheDocument(); + expect(getByTestId(valueTestId)).toHaveTextContent('1 this is a test for red'); + expect(getByTestId(colorTestId)).toBeInTheDocument(); + }); + + it('should only render null when data is null', () => { + const data = null as unknown as InsightsSummaryPanelData[]; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should handle big number in a compact notation', () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + ]; + + const { getByTestId } = render( + + + + ); + + const valueTestId = `${INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID}0`; + expect(getByTestId(valueTestId)).toHaveTextContent('160k this is a test for red'); + }); + + it(`should not show the colored dot if color isn't provided`, () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is a test for no color', + }, + ]; + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx new file mode 100644 index 00000000000000..306eaa101b8046 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { VFC } from 'react'; +import React from 'react'; +import { css } from '@emotion/react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiHealth, EuiPanel } from '@elastic/eui'; +import { FormattedCount } from '../../../common/components/formatted_number'; + +export interface InsightsSummaryPanelData { + /** + * Icon to display on the left side of each row + */ + icon: string; + /** + * Number of results/entries found + */ + value: number; + /** + * Text corresponding of the number of results/entries + */ + text: string; + /** + * Optional parameter for now, will be used to display a dot on the right side + * (corresponding to some sort of severity?) + */ + color?: string; // TODO remove optional when we have guidance on what the colors will actually be +} + +export interface InsightsSummaryPanelProps { + /** + * Array of data to display in each row + */ + data: InsightsSummaryPanelData[]; + /** + * Prefix data-test-subj because this component will be used in multiple places + */ + ['data-test-subj']?: string; +} + +/** + * Panel showing summary information as an icon, a count and text as well as a severity colored dot. + * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results components under the Insights section. + * The colored dot is currently optional but will ultimately be mandatory (waiting on PM and UIUX). + */ +export const InsightsSummaryPanel: VFC = ({ + data, + 'data-test-subj': dataTestSubj, +}) => { + if (!data || data.length === 0) { + return null; + } + + const iconDataTestSubj = `${dataTestSubj}Icon`; + const valueDataTestSubj = `${dataTestSubj}Value`; + const colorDataTestSubj = `${dataTestSubj}Color`; + + return ( + + + {data.map((row, index) => ( + + + + + + {row.text} + + {row.color && ( + + + + )} + + ))} + + + ); +}; + +InsightsSummaryPanel.displayName = 'InsightsSummaryPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index e89c16b3f5f126..9ee38ba2940cb2 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -84,6 +84,18 @@ export const ENTITIES_HOST_OVERVIEW_IP_TEST_ID = export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesHostOverviewRiskLevel'; +/* Insights Threat Intelligence */ + +export const INSIGHTS_THREAT_INTELLIGENCE_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutInsightsThreatIntelligence'; +export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Title`; +export const INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Content`; +export const INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}ViewAllButton`; +export const INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Loading`; +export const INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Icon`; +export const INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Value`; +export const INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Color`; + /* Visualizations section*/ export const VISUALIZATIONS_SECTION_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitle'; export const VISUALIZATIONS_SECTION_HEADER_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx new file mode 100644 index 00000000000000..ecf17f2c7e8221 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { RightPanelContext } from '../context'; +import { + INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID, +} from './test_ids'; +import { TestProviders } from '../../../common/mock'; +import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; +import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; +import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; + +jest.mock('../hooks/use_fetch_threat_intelligence'); + +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + dataFormattedForFieldBrowser: [], +} as unknown as RightPanelContext; + +const renderThreatIntelligenceOverview = (contextValue: RightPanelContext) => ( + + + + + +); + +describe('', () => { + it('should render 1 match detected and 1 field enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 1, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent( + 'Threat Intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '1 threat match detected' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '1 field enriched with threat intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should render 2 matches detected and 2 fields enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 2, + threatEnrichmentsCount: 2, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent( + 'Threat Intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '2 threat matches detected' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '2 fields enriched with threat intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should render 0 field enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 0, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '0 field enriched with threat intelligence' + ); + }); + + it('should render 0 match detected', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 0, + threatEnrichmentsCount: 2, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '0 threat match detected' + ); + }); + + it('should render loading', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: true, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID)).toBeInTheDocument(); + }); + + it('should render null when eventId is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + }); + const contextValue = { + ...panelContextValue, + eventId: null, + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null when dataFormattedForFieldBrowser is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + error: true, + }); + const contextValue = { + ...panelContextValue, + dataFormattedForFieldBrowser: null, + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null when no enrichment found is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 0, + threatEnrichmentsCount: 0, + }); + const contextValue = { + ...panelContextValue, + dataFormattedForFieldBrowser: [], + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should navigate to left section Insights tab when clicking on button', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 1, + }); + const flyoutContextValue = { + openLeftPanel: jest.fn(), + } as unknown as ExpandableFlyoutContext; + + const { getByTestId } = render( + + + + + + + + ); + + getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID).click(); + expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx new file mode 100644 index 00000000000000..63f0862a68b3a5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; +import { InsightsSubSection } from './insights_subsection'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; +import { useRightPanelContext } from '../context'; +import { INSIGHTS_THREAT_INTELLIGENCE_TEST_ID } from './test_ids'; +import { + VIEW_ALL, + THREAT_INTELLIGENCE_TITLE, + THREAT_INTELLIGENCE_TEXT, + THREAT_MATCH_DETECTED, + THREAT_ENRICHMENT, + THREAT_MATCHES_DETECTED, + THREAT_ENRICHMENTS, +} from './translations'; +import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left'; + +/** + * Threat Intelligence section under Insights section, overview tab. + * The component fetches the necessary data, then pass it down to the InsightsSubSection component for loading and error state, + * and the SummaryPanel component for data rendering. + */ +export const ThreatIntelligenceOverview: React.FC = () => { + const { eventId, indexName, dataFormattedForFieldBrowser } = useRightPanelContext(); + const { openLeftPanel } = useExpandableFlyoutContext(); + + const goToThreatIntelligenceTab = useCallback(() => { + openLeftPanel({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: eventId, + indexName, + }, + }); + }, [eventId, openLeftPanel, indexName]); + + const { loading, threatMatchesCount, threatEnrichmentsCount } = useFetchThreatIntelligence({ + dataFormattedForFieldBrowser, + }); + + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: threatMatchesCount, + text: threatMatchesCount <= 1 ? THREAT_MATCH_DETECTED : THREAT_MATCHES_DETECTED, + }, + { + icon: 'warning', + value: threatEnrichmentsCount, + text: threatMatchesCount <= 1 ? THREAT_ENRICHMENT : THREAT_ENRICHMENTS, + }, + ]; + + const error: boolean = + !eventId || + !dataFormattedForFieldBrowser || + (threatMatchesCount === 0 && threatEnrichmentsCount === 0); + + return ( + + + + {VIEW_ALL(THREAT_INTELLIGENCE_TEXT)} + + + ); +}; + +ThreatIntelligenceOverview.displayName = 'ThreatIntelligenceOverview'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index 1a6a2da7344c44..d5b9f0d1928b38 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -101,11 +101,18 @@ export const HIGHLIGHTED_FIELDS_TITLE = i18n.translate( { defaultMessage: 'Highlighted fields' } ); +/* Insights section */ + export const ENTITIES_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.entitiesTitle', { defaultMessage: 'Entities' } ); +export const THREAT_INTELLIGENCE_TITLE = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.threatIntelligenceTitle', + { defaultMessage: 'Threat Intelligence' } +); + export const INSIGHTS_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.insightsTitle', { defaultMessage: 'Insights' } @@ -131,6 +138,41 @@ export const ENTITIES_TEXT = i18n.translate( } ); +export const THREAT_INTELLIGENCE_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText', + { + defaultMessage: 'fields of threat intelligence', + } +); + +export const THREAT_MATCH_DETECTED = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch', + { + defaultMessage: `threat match detected`, + } +); + +export const THREAT_MATCHES_DETECTED = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches', + { + defaultMessage: `threat matches detected`, + } +); + +export const THREAT_ENRICHMENT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment', + { + defaultMessage: `field enriched with threat intelligence`, + } +); + +export const THREAT_ENRICHMENTS = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments', + { + defaultMessage: `fields enriched with threat intelligence`, + } +); + export const VIEW_ALL = (text: string) => i18n.translate('xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton', { values: { text }, diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx new file mode 100644 index 00000000000000..ab57dcebe32aff --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import type { + UseThreatIntelligenceParams, + UseThreatIntelligenceValue, +} from './use_fetch_threat_intelligence'; +import { useFetchThreatIntelligence } from './use_fetch_threat_intelligence'; +import { useInvestigationTimeEnrichment } from '../../../common/containers/cti/event_enrichment'; + +jest.mock('../../../common/containers/cti/event_enrichment'); + +const dataFormattedForFieldBrowser = [ + { + category: 'kibana', + field: 'kibana.alert.rule.uuid', + isObjectArray: false, + originalValue: ['uuid'], + values: ['uuid'], + }, + { + category: 'threat', + field: 'threat.enrichments', + isObjectArray: true, + originalValue: ['{"indicator.file.hash.sha256":["sha256"]}'], + values: ['{"indicator.file.hash.sha256":["sha256"]}'], + }, + { + category: 'threat', + field: 'threat.enrichments.indicator.file.hash.sha256', + isObjectArray: false, + originalValue: ['sha256'], + values: ['sha256'], + }, +]; + +describe('useFetchThreatIntelligence', () => { + let hookResult: RenderHookResult; + + it('should render 1 match detected and 1 field enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 2, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(1); + expect(hookResult.result.current.threatMatchesCount).toEqual(1); + expect(hookResult.result.current.threatEnrichments).toHaveLength(1); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(1); + }); + + it('should render 2 matches detected and 2 fields enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.3'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.4'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 4, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(2); + expect(hookResult.result.current.threatMatchesCount).toEqual(2); + expect(hookResult.result.current.threatEnrichments).toHaveLength(2); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(2); + }); + + it('should render 0 field enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + ], + totalCount: 1, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(1); + expect(hookResult.result.current.threatMatchesCount).toEqual(1); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); + + it('should render 0 match detected', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 1, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toHaveLength(1); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(1); + }); + + it('should return loading true', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: undefined, + loading: true, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); + + it('should return error true', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [], + totalCount: 0, + }, + loading: false, + }); + + hookResult = renderHook(() => + useFetchThreatIntelligence({ dataFormattedForFieldBrowser: null }) + ); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts new file mode 100644 index 00000000000000..4f3d23b082664b --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { groupBy } from 'lodash'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import type { CtiEnrichment } from '../../../../common/search_strategy'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { + filterDuplicateEnrichments, + getEnrichmentFields, + parseExistingEnrichments, + timelineDataToEnrichment, +} from '../../../common/components/event_details/cti_details/helpers'; +import { useInvestigationTimeEnrichment } from '../../../common/containers/cti/event_enrichment'; +import { ENRICHMENT_TYPES } from '../../../../common/cti/constants'; + +export interface UseThreatIntelligenceParams { + /** + * An array of field objects with category and value + */ + dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null; +} + +export interface UseThreatIntelligenceValue { + /** + * Returns true while the threat intelligence data is being queried + */ + loading: boolean; + /** + * Returns true if the dataFormattedForFieldBrowser property is null + */ + error: boolean; + /** + * Threat matches (from an indicator match rule) + */ + threatMatches: CtiEnrichment[]; + /** + * Threat matches count + */ + threatMatchesCount: number; + /** + * Threat enrichments (from the real time query) + */ + threatEnrichments: CtiEnrichment[]; + /** + * Threat enrichments count + */ + threatEnrichmentsCount: number; +} + +/** + * Hook to retrieve threat intelligence data for the expandable flyout right and left sections. + */ +export const useFetchThreatIntelligence = ({ + dataFormattedForFieldBrowser, +}: UseThreatIntelligenceParams): UseThreatIntelligenceValue => { + const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + + // retrieve the threat enrichment fields with value for the current document + // (see https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/common/cti/constants.ts#L35) + const eventFields = useMemo( + () => getEnrichmentFields(dataFormattedForFieldBrowser || []), + [dataFormattedForFieldBrowser] + ); + + // retrieve existing enrichment fields and their value + const existingEnrichments = useMemo( + () => + isAlert + ? parseExistingEnrichments(dataFormattedForFieldBrowser || []).map((enrichmentData) => + timelineDataToEnrichment(enrichmentData) + ) + : [], + [dataFormattedForFieldBrowser, isAlert] + ); + + // api call to retrieve all documents that match the eventFields + const { result: response, loading } = useInvestigationTimeEnrichment(eventFields); + + // combine existing enrichment and enrichment from the api response + // also removes the investigation-time enrichments if the exact indicator already exists + const allEnrichments = useMemo(() => { + if (loading || !response?.enrichments) { + return existingEnrichments; + } + return filterDuplicateEnrichments([...existingEnrichments, ...response.enrichments]); + }, [loading, response, existingEnrichments]); + + // separate threat matches (from indicator-match rule) from threat enrichments (realtime query) + const { + [ENRICHMENT_TYPES.IndicatorMatchRule]: threatMatches, + [ENRICHMENT_TYPES.InvestigationTime]: threatEnrichments, + } = groupBy(allEnrichments, 'matched.type'); + + return { + loading, + error: !dataFormattedForFieldBrowser, + threatMatches, + threatMatchesCount: (threatMatches || []).length, + threatEnrichments, + threatEnrichmentsCount: (threatEnrichments || []).length, + }; +}; From f0106b18179f997e40c704c118e45816c97e2e04 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 21 Apr 2023 14:51:12 -0600 Subject: [PATCH 10/65] [dashboard][maps] fix 'by value' map does not fill dashboard panel on initial page load (#155554) Fixes https://github.com/elastic/kibana/issues/155553 Following steps in issue now renders map as expected Screen Shot 2023-04-21 at 1 18 54 PM --- .../maps/public/connected_components/mb_map/mb_map.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index da4765ca094ecc..71858ecb02459a 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -283,11 +283,10 @@ export class MbMap extends Component { } _initResizerChecker() { + this.state.mbMap?.resize(); // ensure map is sized for container prior to monitoring this._checker = new ResizeChecker(this._containerRef!); this._checker.on('resize', () => { - if (this.state.mbMap) { - this.state.mbMap.resize(); - } + this.state.mbMap?.resize(); }); } From 8039fec178b92f059e7c64cfab0dd0fcdfc87e19 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 21 Apr 2023 16:53:25 -0400 Subject: [PATCH 11/65] [Embeddable] Always send value input from edit panel action (#155283) ensures that the by value input is passed into the editors regardless of whether the panel is by value or by reference. --- .../lib/actions/edit_panel_action.test.tsx | 30 ++----------------- .../public/lib/actions/edit_panel_action.ts | 5 +--- x-pack/plugins/maps/public/render_app.tsx | 3 +- 3 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 926b6782b5633d..d2fb4e701e4b41 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -7,7 +7,7 @@ */ import { EditPanelAction } from './edit_panel_action'; -import { Embeddable, EmbeddableInput, SavedObjectEmbeddableInput } from '../embeddables'; +import { Embeddable, EmbeddableInput } from '../embeddables'; import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples'; import { embeddablePluginMock } from '../../mocks'; @@ -42,7 +42,7 @@ test('is compatible when edit url is available, in edit mode and editable', asyn ).toBe(true); }); -test('redirects to app using state transfer with by value mode', async () => { +test('redirects to app using state transfer', async () => { applicationMock.currentAppId$ = of('superCoolCurrentApp'); const testPath = '/test-path'; const action = new EditPanelAction( @@ -78,32 +78,6 @@ test('redirects to app using state transfer with by value mode', async () => { }); }); -test('redirects to app using state transfer without by value mode', async () => { - applicationMock.currentAppId$ = of('superCoolCurrentApp'); - const testPath = '/test-path'; - const action = new EditPanelAction( - getFactory, - applicationMock, - stateTransferMock, - () => testPath - ); - const embeddable = new EditableEmbeddable( - { id: '123', viewMode: ViewMode.EDIT, savedObjectId: '1234' } as SavedObjectEmbeddableInput, - true - ); - embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' })); - await action.execute({ embeddable }); - expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', { - path: '/123', - state: { - originatingApp: 'superCoolCurrentApp', - embeddableId: '123', - valueInput: undefined, - originatingPath: testPath, - }, - }); -}); - test('getHref returns the edit urls', async () => { const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock); expect(action.getHref).toBeDefined(); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 1dcecf4ac894d1..ba59d92cbef60c 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -17,7 +17,6 @@ import { IEmbeddable, EmbeddableEditorState, EmbeddableStateTransfer, - SavedObjectEmbeddableInput, EmbeddableInput, Container, } from '../..'; @@ -124,13 +123,11 @@ export class EditPanelAction implements Action { if (app && path) { if (this.currentAppId) { - const byValueMode = !(embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId; - const originatingPath = this.getOriginatingPath?.(); const state: EmbeddableEditorState = { originatingApp: this.currentAppId, - valueInput: byValueMode ? this.getExplicitInput({ embeddable }) : undefined, + valueInput: this.getExplicitInput({ embeddable }), embeddableId: embeddable.id, searchSessionId: embeddable.getInput().searchSessionId, originatingPath, diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index 01eba4ee7905ef..c4bddf5a715410 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -86,8 +86,7 @@ export async function renderApp( mapEmbeddableInput = { savedObjectId: routeProps.match.params.savedMapId, } as MapByReferenceInput; - } - if (valueInput) { + } else if (valueInput) { mapEmbeddableInput = valueInput as MapByValueInput; } From 9b6c6bcd9c8eb251e3f186d8e5038a18035de465 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Fri, 21 Apr 2023 15:59:20 -0500 Subject: [PATCH 12/65] [Security Solution] expandable flyout small cleanup (#155515) --- .../components/highlighted_fields.test.tsx | 24 +++++-------------- .../right/components/highlighted_fields.tsx | 2 +- .../right/components/mitre_attack.test.tsx | 8 ++----- .../flyout/right/components/reason.test.tsx | 24 +++++-------------- .../right/components/risk_score.test.tsx | 16 ++++--------- .../flyout/right/components/severity.test.tsx | 24 +++++-------------- 6 files changed, 25 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx index 52a85b66a31085..0045f30cecebc2 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx @@ -54,7 +54,7 @@ describe('', () => { browserFields: {}, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( @@ -62,11 +62,7 @@ describe('', () => { ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if browserFields is null', () => { @@ -78,7 +74,7 @@ describe('', () => { browserFields: null, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( @@ -86,11 +82,7 @@ describe('', () => { ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if eventId is null', () => { @@ -102,7 +94,7 @@ describe('', () => { browserFields: {}, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( @@ -110,10 +102,6 @@ describe('', () => { ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx index af14a20788466d..eaae42100ded3d 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx @@ -33,7 +33,7 @@ export const HighlightedFields: FC = () => { }, [eventId, indexName, openRightPanel, scopeId]); if (!dataFormattedForFieldBrowser || !browserFields || !eventId) { - return <>; + return null; } return ( diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/mitre_attack.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/mitre_attack.test.tsx index f0bc9bd993f66c..24e52e4ba59158 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/mitre_attack.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/mitre_attack.test.tsx @@ -33,16 +33,12 @@ describe('', () => { }, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx index d2a7c90011d688..b7050d1df0fa03 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx @@ -37,17 +37,13 @@ describe('', () => { dataAsNestedObject: {}, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render null if dataAsNestedObject is null', () => { @@ -55,17 +51,13 @@ describe('', () => { dataFormattedForFieldBrowser: [], } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render null if renderer is null', () => { const panelContextValue = { @@ -73,16 +65,12 @@ describe('', () => { dataFormattedForFieldBrowser: [], } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/risk_score.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/risk_score.test.tsx index 3b9364df1dd040..554b6c90db32a0 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/risk_score.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/risk_score.test.tsx @@ -38,17 +38,13 @@ describe('', () => { getFieldsData: jest.fn(), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if getFieldsData is invalid', () => { @@ -56,16 +52,12 @@ describe('', () => { getFieldsData: jest.fn().mockImplementation(() => 123), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx index e329459c6cbc17..92c8b0a3826388 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx @@ -38,17 +38,13 @@ describe('', () => { getFieldsData: jest.fn(), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if getFieldsData is invalid array', () => { @@ -56,17 +52,13 @@ describe('', () => { getFieldsData: jest.fn().mockImplementation(() => ['abc']), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if getFieldsData is invalid string', () => { @@ -74,16 +66,12 @@ describe('', () => { getFieldsData: jest.fn().mockImplementation(() => 'abc'), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); From 9bb127f26c7edec602f9da0efb58861cb47af9cc Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:01:19 -0600 Subject: [PATCH 13/65] [RAM] Add window maintenance column to rule logs and rule tables (#155333) ## Summary Resolves: https://github.com/elastic/kibana/issues/153775 This PR adds the `maintenance_window_id` column to the following tables: ## O11Y/Security solutions Alerts table ![o11y-alerts-table](https://user-images.githubusercontent.com/74562234/233227165-03c105d9-3890-4462-91ec-cd7c6ad26d7b.png) ## Rule details alerts table ![rule_details_maintenance_window_ids](https://user-images.githubusercontent.com/74562234/233226920-6f903ddf-401f-49e7-bb9c-9a36334fc7ce.png) ## Rule run event log ![rule_event_log_maintenance_window](https://user-images.githubusercontent.com/74562234/233226784-c6e804e6-eabe-4500-b51a-aae7aafbcff1.png) ## To test: 1. Set `ENABLE_MAINTENANCE_WINDOWS` to true in `x-pack/plugins/alerting/public/plugin.ts` 2. Create 1 or more active maintenance windows in stack management 3. Create a rule, trigger some alerts 4. Go to the rule details page alerts table, assert the `maintenance window` column is there, and renders the maintenance window ids 5. Go to the rule details page event log table, assert the `maintenance window` column can enabled, and renders the maintenance window ids 6. Create a O11Y rule, trigger some alerts 7. Go to the O11Y alerts table, assert that the `maintenance_window_id` field can be enabled, and renders the maintenance window ids ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Lisa Cawley Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/alerting/common/alert_summary.ts | 1 + .../alerting/common/execution_log_types.ts | 1 + .../lib/alert_summary_from_event_log.ts | 4 + .../alerting_event_logger.mock.ts | 1 + .../alerting_event_logger.test.ts | 45 ++++++++++ .../alerting_event_logger.ts | 29 +++++- .../lib/get_execution_log_aggregation.test.ts | 45 +++++++--- .../lib/get_execution_log_aggregation.ts | 38 +++++--- .../routes/get_global_execution_logs.test.ts | 2 + .../routes/get_rule_execution_log.test.ts | 2 + .../tests/get_execution_log.test.ts | 11 ++- .../server/task_runner/task_runner.test.ts | 14 ++- .../server/task_runner/task_runner.ts | 3 + .../public/application/constants/index.ts | 1 + .../event_log_list_cell_renderer.test.tsx | 10 +++ .../event_log_list_cell_renderer.tsx | 4 + .../sections/rule_details/components/rule.tsx | 1 + .../components/rule_alert_list.tsx | 15 ++++ .../components/rule_event_log_list_table.tsx | 14 +++ .../sections/rule_details/components/types.ts | 1 + .../tests/alerting/group1/event_log.ts | 7 +- .../alerting/group1/get_alert_summary.ts | 88 +++++++++++++++++++ 22 files changed, 307 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/alerting/common/alert_summary.ts b/x-pack/plugins/alerting/common/alert_summary.ts index b1563882d4f21e..a465d6af2daede 100644 --- a/x-pack/plugins/alerting/common/alert_summary.ts +++ b/x-pack/plugins/alerting/common/alert_summary.ts @@ -38,4 +38,5 @@ export interface AlertStatus { actionGroupId?: string; activeStartDate?: string; flapping: boolean; + maintenanceWindowIds?: string[]; } diff --git a/x-pack/plugins/alerting/common/execution_log_types.ts b/x-pack/plugins/alerting/common/execution_log_types.ts index 2d5e34df8f7662..7a35bea0df6196 100644 --- a/x-pack/plugins/alerting/common/execution_log_types.ts +++ b/x-pack/plugins/alerting/common/execution_log_types.ts @@ -63,6 +63,7 @@ export interface IExecutionLog { rule_id: string; space_ids: string[]; rule_name: string; + maintenance_window_ids: string[]; } export interface IExecutionErrors { diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts index 7c3df21a3281d5..9dc4a4fec5f8ea 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts @@ -86,6 +86,10 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams) status.flapping = true; } + if (event?.kibana?.alert?.maintenance_window_ids?.length) { + status.maintenanceWindowIds = event.kibana.alert.maintenance_window_ids as string[]; + } + switch (action) { case EVENT_LOG_ACTIONS.newInstance: status.activeStartDate = timeStamp; diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.mock.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.mock.ts index ff62ddaea7ff4e..00a9bf7221ef32 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.mock.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.mock.ts @@ -15,6 +15,7 @@ const createAlertingEventLoggerMock = () => { setRuleName: jest.fn(), setExecutionSucceeded: jest.fn(), setExecutionFailed: jest.fn(), + setMaintenanceWindowIds: jest.fn(), logTimeout: jest.fn(), logAlert: jest.fn(), logAction: jest.fn(), diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 2711f921e81ec6..62c8d9eed29fdf 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -284,6 +284,51 @@ describe('AlertingEventLogger', () => { }); }); + describe('setMaintenanceWindowIds()', () => { + test('should throw error if alertingEventLogger has not been initialized', () => { + expect(() => + alertingEventLogger.setMaintenanceWindowIds([]) + ).toThrowErrorMatchingInlineSnapshot(`"AlertingEventLogger not initialized"`); + }); + + test('should throw error if event is null', () => { + alertingEventLogger.initialize(context); + expect(() => + alertingEventLogger.setMaintenanceWindowIds([]) + ).toThrowErrorMatchingInlineSnapshot(`"AlertingEventLogger not initialized"`); + }); + + it('should update event maintenance window IDs correctly', () => { + alertingEventLogger.initialize(context); + alertingEventLogger.start(); + alertingEventLogger.setMaintenanceWindowIds([]); + + const event = initializeExecuteRecord(contextWithScheduleDelay); + expect(alertingEventLogger.getEvent()).toEqual({ + ...event, + kibana: { + ...event.kibana, + alert: { + ...event.kibana?.alert, + maintenance_window_ids: [], + }, + }, + }); + + alertingEventLogger.setMaintenanceWindowIds(['test-id-1', 'test-id-2']); + expect(alertingEventLogger.getEvent()).toEqual({ + ...event, + kibana: { + ...event.kibana, + alert: { + ...event.kibana?.alert, + maintenance_window_ids: ['test-id-1', 'test-id-2'], + }, + }, + }); + }); + }); + describe('logTimeout()', () => { test('should throw error if alertingEventLogger has not been initialized', () => { expect(() => alertingEventLogger.logTimeout()).toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index 37029b4c967032..60d9d21f161131 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -138,6 +138,14 @@ export class AlertingEventLogger { updateEvent(this.event, { message, outcome: 'success', alertingOutcome: 'success' }); } + public setMaintenanceWindowIds(maintenanceWindowIds: string[]) { + if (!this.isInitialized || !this.event) { + throw new Error('AlertingEventLogger not initialized'); + } + + updateEvent(this.event, { maintenanceWindowIds }); + } + public setExecutionFailed(message: string, errorMessage: string) { if (!this.isInitialized || !this.event) { throw new Error('AlertingEventLogger not initialized'); @@ -351,11 +359,22 @@ interface UpdateEventOpts { reason?: string; metrics?: RuleRunMetrics; timings?: TaskRunnerTimings; + maintenanceWindowIds?: string[]; } export function updateEvent(event: IEvent, opts: UpdateEventOpts) { - const { message, outcome, error, ruleName, status, reason, metrics, timings, alertingOutcome } = - opts; + const { + message, + outcome, + error, + ruleName, + status, + reason, + metrics, + timings, + alertingOutcome, + maintenanceWindowIds, + } = opts; if (!event) { throw new Error('Cannot update event because it is not initialized.'); } @@ -431,4 +450,10 @@ export function updateEvent(event: IEvent, opts: UpdateEventOpts) { ...timings, }; } + + if (maintenanceWindowIds) { + event.kibana = event.kibana || {}; + event.kibana.alert = event.kibana.alert || {}; + event.kibana.alert.maintenance_window_ids = maintenanceWindowIds; + } } diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts index 6a57baaacef27f..bb38fb7a98bfa5 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts @@ -270,7 +270,7 @@ describe('getExecutionLogAggregation', () => { }, }, executionDuration: { max: { field: 'event.duration' } }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { top_hits: { size: 1, _source: { @@ -283,6 +283,7 @@ describe('getExecutionLogAggregation', () => { 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', + 'kibana.alert.maintenance_window_ids', ], }, }, @@ -477,7 +478,7 @@ describe('getExecutionLogAggregation', () => { }, }, executionDuration: { max: { field: 'event.duration' } }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { top_hits: { size: 1, _source: { @@ -490,6 +491,7 @@ describe('getExecutionLogAggregation', () => { 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', + 'kibana.alert.maintenance_window_ids', ], }, }, @@ -684,7 +686,7 @@ describe('getExecutionLogAggregation', () => { }, }, executionDuration: { max: { field: 'event.duration' } }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { top_hits: { size: 1, _source: { @@ -697,6 +699,7 @@ describe('getExecutionLogAggregation', () => { 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', + 'kibana.alert.maintenance_window_ids', ], }, }, @@ -774,7 +777,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 0.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -861,7 +864,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -880,6 +883,9 @@ describe('formatExecutionLogResult', () => { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -958,6 +964,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -981,6 +988,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); @@ -1022,7 +1030,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 0.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1112,7 +1120,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1131,6 +1139,9 @@ describe('formatExecutionLogResult', () => { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -1209,6 +1220,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -1232,6 +1244,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); @@ -1273,7 +1286,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 0.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1355,7 +1368,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1374,6 +1387,9 @@ describe('formatExecutionLogResult', () => { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -1452,6 +1468,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -1475,6 +1492,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); @@ -1516,7 +1534,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1603,7 +1621,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1622,6 +1640,9 @@ describe('formatExecutionLogResult', () => { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -1700,6 +1721,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: [], }, { id: '61bb867b-661a-471f-bf92-23471afa10b3', @@ -1723,6 +1745,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts index b65499de20d453..44e1f7bbe98c10 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts @@ -41,6 +41,7 @@ const NUMBER_OF_NEW_ALERTS_FIELD = 'kibana.alert.rule.execution.metrics.alert_co const NUMBER_OF_RECOVERED_ALERTS_FIELD = 'kibana.alert.rule.execution.metrics.alert_counts.recovered'; const EXECUTION_UUID_FIELD = 'kibana.alert.rule.execution.uuid'; +const MAINTENANCE_WINDOW_IDS_FIELD = 'kibana.alert.maintenance_window_ids'; const Millis2Nanos = 1000 * 1000; @@ -82,7 +83,8 @@ interface IExecutionUuidAggBucket extends estypes.AggregationsStringTermsBucketK numActiveAlerts: estypes.AggregationsMaxAggregate; numRecoveredAlerts: estypes.AggregationsMaxAggregate; numNewAlerts: estypes.AggregationsMaxAggregate; - outcomeAndMessage: estypes.AggregationsTopHitsAggregate; + outcomeMessageAndMaintenanceWindow: estypes.AggregationsTopHitsAggregate; + maintenanceWindowIds: estypes.AggregationsTopHitsAggregate; }; actionExecution: { actionOutcomes: IActionExecution; @@ -401,7 +403,7 @@ export function getExecutionLogAggregation({ field: DURATION_FIELD, }, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { top_hits: { size: 1, _source: { @@ -414,6 +416,7 @@ export function getExecutionLogAggregation({ SPACE_ID_FIELD, RULE_NAME_FIELD, ALERTING_OUTCOME_FIELD, + MAINTENANCE_WINDOW_IDS_FIELD, ], }, }, @@ -485,20 +488,30 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio const actionExecutionError = actionExecutionOutcomes.find((subBucket) => subBucket?.key === 'failure')?.doc_count ?? 0; - const outcomeAndMessage = bucket?.ruleExecution?.outcomeAndMessage?.hits?.hits[0]?._source ?? {}; - let status = outcomeAndMessage.kibana?.alerting?.outcome ?? ''; + const outcomeMessageAndMaintenanceWindow = + bucket?.ruleExecution?.outcomeMessageAndMaintenanceWindow?.hits?.hits[0]?._source ?? {}; + let status = outcomeMessageAndMaintenanceWindow.kibana?.alerting?.outcome ?? ''; if (isEmpty(status)) { - status = outcomeAndMessage.event?.outcome ?? ''; + status = outcomeMessageAndMaintenanceWindow.event?.outcome ?? ''; } - const outcomeMessage = outcomeAndMessage.message ?? ''; - const outcomeErrorMessage = outcomeAndMessage.error?.message ?? ''; + const outcomeMessage = outcomeMessageAndMaintenanceWindow.message ?? ''; + const outcomeErrorMessage = outcomeMessageAndMaintenanceWindow.error?.message ?? ''; const message = status === 'failure' ? `${outcomeMessage} - ${outcomeErrorMessage}` : outcomeMessage; - const version = outcomeAndMessage.kibana?.version ?? ''; - - const ruleId = outcomeAndMessage ? outcomeAndMessage?.rule?.id ?? '' : ''; - const spaceIds = outcomeAndMessage ? outcomeAndMessage?.kibana?.space_ids ?? [] : []; - const ruleName = outcomeAndMessage ? outcomeAndMessage.rule?.name ?? '' : ''; + const version = outcomeMessageAndMaintenanceWindow.kibana?.version ?? ''; + + const ruleId = outcomeMessageAndMaintenanceWindow + ? outcomeMessageAndMaintenanceWindow?.rule?.id ?? '' + : ''; + const spaceIds = outcomeMessageAndMaintenanceWindow + ? outcomeMessageAndMaintenanceWindow?.kibana?.space_ids ?? [] + : []; + const maintenanceWindowIds = outcomeMessageAndMaintenanceWindow + ? outcomeMessageAndMaintenanceWindow.kibana?.alert?.maintenance_window_ids ?? [] + : []; + const ruleName = outcomeMessageAndMaintenanceWindow + ? outcomeMessageAndMaintenanceWindow.rule?.name ?? '' + : ''; return { id: bucket?.key ?? '', timestamp: bucket?.ruleExecution?.executeStartTime.value_as_string ?? '', @@ -520,6 +533,7 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio rule_id: ruleId, space_ids: spaceIds, rule_name: ruleName, + maintenance_window_ids: maintenanceWindowIds, }; } diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts index 3ee2b0d1816ba4..f6e1c3417a42ff 100644 --- a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts @@ -48,6 +48,7 @@ describe('getRuleExecutionLogRoute', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: ['namespace'], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -71,6 +72,7 @@ describe('getRuleExecutionLogRoute', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: ['namespace'], + maintenance_window_ids: ['test-id-1'], }, ], }; diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts index eb22a6429809a0..a0dd57da558eb5 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts @@ -49,6 +49,7 @@ describe('getRuleExecutionLogRoute', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: ['namespace'], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -72,6 +73,7 @@ describe('getRuleExecutionLogRoute', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: ['namespace'], + maintenance_window_ids: ['test-id-1'], }, ], }; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts index cebe9fa963971f..33fb19c40f7fdb 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts @@ -133,7 +133,7 @@ const aggregateResults = { numRecoveredAlerts: { value: 0.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -242,7 +242,7 @@ const aggregateResults = { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -261,6 +261,9 @@ const aggregateResults = { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -389,6 +392,7 @@ describe('getExecutionLogForRule()', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -412,6 +416,7 @@ describe('getExecutionLogForRule()', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); @@ -725,6 +730,7 @@ describe('getGlobalExecutionLogWithAuth()', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -748,6 +754,7 @@ describe('getGlobalExecutionLogWithAuth()', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index ea37e5d8594d6e..216cf19373cfa6 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -678,11 +678,14 @@ describe('Task Runner', () => { 'Updating rule task for test rule with id 1 - {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"} - {"outcome":"succeeded","outcomeOrder":0,"outcomeMsg":null,"warning":null,"alertsCount":{"active":1,"new":1,"recovered":0,"ignored":0}}' ); + const maintenanceWindowIds = ['test-id-1', 'test-id-2']; + testAlertingEventLogCalls({ activeAlerts: 1, newAlerts: 1, status: 'active', logAlert: 2, + maintenanceWindowIds, }); expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( 1, @@ -690,7 +693,7 @@ describe('Task Runner', () => { action: EVENT_LOG_ACTIONS.newInstance, group: 'default', state: { start: DATE_1970, duration: '0' }, - maintenanceWindowIds: ['test-id-1', 'test-id-2'], + maintenanceWindowIds, }) ); expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( @@ -699,7 +702,7 @@ describe('Task Runner', () => { action: EVENT_LOG_ACTIONS.activeInstance, group: 'default', state: { start: DATE_1970, duration: '0' }, - maintenanceWindowIds: ['test-id-1', 'test-id-2'], + maintenanceWindowIds, }) ); @@ -3113,6 +3116,7 @@ describe('Task Runner', () => { errorMessage = 'GENERIC ERROR MESSAGE', executionStatus = 'succeeded', setRuleName = true, + maintenanceWindowIds, logAlert = 0, logAction = 0, hasReachedAlertLimit = false, @@ -3126,6 +3130,7 @@ describe('Task Runner', () => { generatedActions?: number; executionStatus?: 'succeeded' | 'failed' | 'not-reached'; setRuleName?: boolean; + maintenanceWindowIds?: string[]; logAlert?: number; logAction?: number; errorReason?: string; @@ -3140,6 +3145,11 @@ describe('Task Runner', () => { expect(alertingEventLogger.setRuleName).not.toHaveBeenCalled(); } expect(alertingEventLogger.getStartAndDuration).toHaveBeenCalled(); + if (maintenanceWindowIds?.length) { + expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith( + maintenanceWindowIds + ); + } if (status === 'error') { expect(alertingEventLogger.done).toHaveBeenCalledWith({ metrics: null, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 41426724187e15..b4ee45eea902bf 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -332,6 +332,9 @@ export class TaskRunner< const maintenanceWindowIds = activeMaintenanceWindows.map( (maintenanceWindow) => maintenanceWindow.id ); + if (maintenanceWindowIds.length) { + this.alertingEventLogger.setMaintenanceWindowIds(maintenanceWindowIds); + } const { updatedRuleTypeState } = await this.timer.runWithTimer( TaskRunnerTimerSpan.RuleTypeRun, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 19b66ff62d5244..22ead5ca4d2fe9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -68,6 +68,7 @@ export const RULE_EXECUTION_LOG_COLUMN_IDS = [ 'es_search_duration', 'schedule_delay', 'timed_out', + 'maintenance_window_ids', ] as const; export const RULE_EXECUTION_LOG_DURATION_COLUMNS = [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.test.tsx index 0ec383c84f6e64..32e5c0e83d2972 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.test.tsx @@ -111,6 +111,16 @@ describe('rule_event_log_list_cell_renderer', () => { expect(wrapper.find(EuiIcon).props().color).toEqual('gray'); }); + it('renders maintenance window correctly', () => { + const wrapper = shallow( + + ); + expect(wrapper.text()).toEqual('test-id-1, test-id-2'); + }); + it('links to rules on the correct space', () => { const wrapper1 = shallow( {value ? 'true' : 'false'}; } + if (columnId === 'maintenance_window_ids') { + return <>{Array.isArray(value) ? value.join(', ') : ''}; + } + return <>{value}; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index 81c5e0d7d7e906..8b99cc910d8daf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -197,6 +197,7 @@ export function alertToListItem( isMuted, sortPriority, flapping: alert.flapping, + ...(alert.maintenanceWindowIds ? { maintenanceWindowIds: alert.maintenanceWindowIds } : {}), }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx index 3b3142181d8e72..5a6fad3b939a28 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx @@ -91,6 +91,21 @@ const alertsTableColumns = ( width: '80px', 'data-test-subj': 'alertsTableCell-duration', }, + { + field: 'maintenanceWindowIds', + width: '250px', + render: (value: string[]) => { + return Array.isArray(value) ? value.join(', ') : ''; + }, + name: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.maintenanceWindowIds', + { + defaultMessage: 'Maintenance windows', + } + ), + sortable: false, + 'data-test-subj': 'alertsTableCell-maintenanceWindowIds', + }, { field: '', align: RIGHT_ALIGNMENT, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx index 7df89d799b2ae4..7965f58fa84203 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx @@ -610,6 +610,20 @@ export const RuleEventLogListTable = ( ), isSortable: getIsColumnSortable('timed_out'), }, + { + id: 'maintenance_window_ids', + displayAsText: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.maintenanceWindowIds', + { + defaultMessage: 'Maintenance windows', + } + ), + actions: { + showSortAsc: false, + showSortDesc: false, + }, + isSortable: getIsColumnSortable('maintenance_window_ids'), + }, ], [getPaginatedRowIndex, onFlyoutOpen, onFilterChange, hasRuleNames, showFromAllSpaces, logs] ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/types.ts index c469b61690c3a7..f66f648051715f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/types.ts @@ -14,4 +14,5 @@ export interface AlertListItem { isMuted: boolean; sortPriority: number; flapping: boolean; + maintenanceWindowIds?: string[]; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts index fac54e789bd306..4e3a3fb70a4551 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts @@ -1293,7 +1293,12 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); }); - const actionsToCheck = ['new-instance', 'active-instance', 'recovered-instance']; + const actionsToCheck = [ + 'new-instance', + 'active-instance', + 'recovered-instance', + 'execute', + ]; events.forEach((event) => { if (actionsToCheck.includes(event?.event?.action || '')) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts index 75fd92ec61eaa0..6f8de29437621c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts @@ -268,6 +268,94 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo expect(actualAlerts).to.eql(expectedAlerts); }); + it('handles multi-alert status during maintenance window', async () => { + // pattern of when the rule should fire + const pattern = { + alertA: [true, true, true, true], + alertB: [true, true, false, false], + alertC: [true, true, true, true], + }; + + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.patternFiring', + params: { pattern }, + schedule: { interval: '1s' }, + }) + ) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + const { body: createdMaintenanceWindow } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send({ + title: 'test-maintenance-window', + duration: 60 * 60 * 1000, // 1 hr + r_rule: { + dtstart: new Date().toISOString(), + tzid: 'UTC', + freq: 2, // weekly + }, + }); + + objectRemover.add( + Spaces.space1.id, + createdMaintenanceWindow.id, + 'rules/maintenance_window', + 'alerting', + true + ); + + await alertUtils.muteInstance(createdRule.id, 'alertC'); + await alertUtils.muteInstance(createdRule.id, 'alertD'); + await waitForEvents(createdRule.id, ['new-instance', 'recovered-instance']); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` + ); + + const actualAlerts = checkAndCleanActualAlerts(response.body.alerts, [ + 'alertA', + 'alertB', + 'alertC', + ]); + + const expectedAlerts = { + alertA: { + status: 'Active', + muted: false, + actionGroupId: 'default', + activeStartDate: actualAlerts.alertA.activeStartDate, + flapping: false, + maintenanceWindowIds: [createdMaintenanceWindow.id], + }, + alertB: { + status: 'OK', + muted: false, + flapping: false, + maintenanceWindowIds: [createdMaintenanceWindow.id], + }, + alertC: { + status: 'Active', + muted: true, + actionGroupId: 'default', + activeStartDate: actualAlerts.alertC.activeStartDate, + flapping: false, + maintenanceWindowIds: [createdMaintenanceWindow.id], + }, + alertD: { + status: 'OK', + muted: true, + flapping: false, + }, + }; + expect(actualAlerts).to.eql(expectedAlerts); + }); + describe('legacy', () => { it('handles multi-alert status', async () => { // pattern of when the alert should fire From fdc23f570e46d48d9910926349554277c2e49b59 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Fri, 21 Apr 2023 16:15:25 -0500 Subject: [PATCH 14/65] [data view field editor] Move preview response and value formatter to controller (#153799) ## Summary This is a refactor that moves logic from hooks to a controller class for the data view field editor preview. Functionality is unaffected. --- .../helpers/setup_environment.tsx | 2 +- .../field_editor/form_fields/script_field.tsx | 8 +- .../components/field_editor/form_schema.ts | 4 +- .../field_editor_flyout_content_container.tsx | 2 +- .../components/preview/field_preview.tsx | 5 +- .../preview/field_preview_context.tsx | 161 ++++++------------ ...w_controller.ts => preview_controller.tsx} | 67 +++++++- .../public/components/preview/types.ts | 7 +- 8 files changed, 136 insertions(+), 120 deletions(-) rename src/plugins/data_view_field_editor/public/components/preview/{preview_controller.ts => preview_controller.tsx} (60%) diff --git a/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx b/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx index e356c76ddb9896..e76cf6960cc233 100644 --- a/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx +++ b/src/plugins/data_view_field_editor/__jest__/client_integration/helpers/setup_environment.tsx @@ -149,7 +149,7 @@ export const WithFieldEditorDependencies = }; const mergedDependencies = merge({}, dependencies, overridingDependencies); - const previewController = new PreviewController({ dataView, search }); + const previewController = new PreviewController({ dataView, search, fieldFormats }); return ( diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx index fddf14c864743b..077471b44c237a 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -24,7 +24,7 @@ import { } from '../../../shared_imports'; import type { RuntimeFieldPainlessError } from '../../../types'; import { painlessErrorToMonacoMarker } from '../../../lib'; -import { useFieldPreviewContext, Context } from '../../preview'; +import { useFieldPreviewContext } from '../../preview'; import { schema } from '../form_schema'; import type { FieldFormInternal } from '../field_editor'; import { useStateSelector } from '../../../state_utils'; @@ -57,6 +57,7 @@ const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessConte const currentDocumentSelector = (state: PreviewState) => state.documents[state.currentIdx]; const currentDocumentIsLoadingSelector = (state: PreviewState) => state.isLoadingDocuments; +const currentErrorSelector = (state: PreviewState) => state.previewResponse?.error; const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Props) => { const { @@ -66,14 +67,15 @@ const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Pr const editorValidationSubscription = useRef(); const fieldCurrentValue = useRef(''); - const { error, isLoadingPreview, isPreviewAvailable, controller } = useFieldPreviewContext(); + const { isLoadingPreview, isPreviewAvailable, controller } = useFieldPreviewContext(); + const error = useStateSelector(controller.state$, currentErrorSelector); const currentDocument = useStateSelector(controller.state$, currentDocumentSelector); const isFetchingDoc = useStateSelector(controller.state$, currentDocumentIsLoadingSelector); const [validationData$, nextValidationData$] = useBehaviorSubject< | { isFetchingDoc: boolean; isLoadingPreview: boolean; - error: Context['error']; + error: PreviewState['previewResponse']['error']; } | undefined >(undefined); diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts b/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts index 391f54581f2588..45ecac570dd997 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_schema.ts @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption } from '@elastic/eui'; import { fieldValidators, FieldConfig, RuntimeType, ValidationFunc } from '../../shared_imports'; -import type { Context } from '../preview'; import { RUNTIME_FIELD_OPTIONS } from './constants'; +import type { PreviewState } from '../preview/types'; const { containsCharsField, emptyField, numberGreaterThanField } = fieldValidators; const i18nTexts = { @@ -25,7 +25,7 @@ const i18nTexts = { // Validate the painless **script** const painlessScriptValidator: ValidationFunc = async ({ customData: { provider } }) => { - const previewError = (await provider()) as Context['error']; + const previewError = (await provider()) as PreviewState['previewResponse']['error']; if (previewError && previewError.code === 'PAINLESS_SCRIPT_ERROR') { return { diff --git a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx index 60903fae03ea1f..c763d08ae470b1 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx @@ -85,7 +85,7 @@ export const FieldEditorFlyoutContentContainer = ({ fieldFormats, uiSettings, }: Props) => { - const [controller] = useState(() => new PreviewController({ dataView, search })); + const [controller] = useState(() => new PreviewController({ dataView, search, fieldFormats })); const [isSaving, setIsSaving] = useState(false); const { fields } = dataView; diff --git a/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx b/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx index 672f0a747991d2..005a2c634cf844 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx +++ b/src/plugins/data_view_field_editor/public/components/preview/field_preview.tsx @@ -16,6 +16,7 @@ import { DocumentsNavPreview } from './documents_nav_preview'; import { FieldPreviewError } from './field_preview_error'; import { PreviewListItem } from './field_list/field_list_item'; import { PreviewFieldList } from './field_list/field_list'; +import { useStateSelector } from '../../state_utils'; import './field_preview.scss'; @@ -28,12 +29,12 @@ export const FieldPreview = () => { value: { name, script, format }, }, isLoadingPreview, - fields, - error, documents: { fetchDocError }, reset, isPreviewAvailable, + controller, } = useFieldPreviewContext(); + const { fields, error } = useStateSelector(controller.state$, (state) => state.previewResponse); // To show the preview we at least need a name to be defined, the script or the format // and an first response from the _execute API diff --git a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx index f554025ce9f4bf..c4cdfad7d4fece 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx +++ b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx @@ -20,7 +20,6 @@ import { renderToString } from 'react-dom/server'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { castEsToKbnFieldTypeName } from '@kbn/field-types'; import { BehaviorSubject } from 'rxjs'; import { RuntimePrimitiveTypes } from '../../shared_imports'; import { useStateSelector } from '../../state_utils'; @@ -32,7 +31,6 @@ import type { Context, Params, EsDocument, - ScriptErrorCodes, FetchDocError, FieldPreview, PreviewState, @@ -101,17 +99,11 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro notifications, api: { getFieldPreview }, }, - fieldFormats, fieldName$, } = useFieldEditorContext(); const fieldPreview$ = useRef(new BehaviorSubject(undefined)); - /** Response from the Painless _execute API */ - const [previewResponse, setPreviewResponse] = useState<{ - fields: Context['fields']; - error: Context['error']; - }>({ fields: [], error: null }); const [initialPreviewComplete, setInitialPreviewComplete] = useState(false); /** Possible error while fetching sample documents */ @@ -169,45 +161,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro ); }, [type, script, currentDocId]); - const setPreviewError = useCallback((error: Context['error']) => { - setPreviewResponse((prev) => ({ - ...prev, - error, - })); - }, []); - - const clearPreviewError = useCallback((errorCode: ScriptErrorCodes) => { - setPreviewResponse((prev) => { - const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error; - return { - ...prev, - error, - }; - }); - }, []); - - const valueFormatter = useCallback( - (value: unknown) => { - if (format?.id) { - const formatter = fieldFormats.getInstance(format.id, format.params); - if (formatter) { - return formatter.getConverterFor('html')(value) ?? JSON.stringify(value); - } - } - - if (type) { - const fieldType = castEsToKbnFieldTypeName(type); - const defaultFormatterForType = fieldFormats.getDefaultInstance(fieldType); - if (defaultFormatterForType) { - return defaultFormatterForType.getConverterFor('html')(value) ?? JSON.stringify(value); - } - } - - return defaultValueFormatter(value); - }, - [format, type, fieldFormats] - ); - const fetchSampleDocuments = useCallback( async (limit: number = 50) => { if (typeof limit !== 'number') { @@ -217,7 +170,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro lastExecutePainlessRequestParams.current.documentId = undefined; setIsFetchingDocument(true); - setPreviewResponse({ fields: [], error: null }); + controller.setPreviewResponse({ fields: [], error: null }); const [response, searchError] = await search .search({ @@ -335,14 +288,14 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro const updateSingleFieldPreview = useCallback( (fieldName: string, values: unknown[]) => { const [value] = values; - const formattedValue = valueFormatter(value); + const formattedValue = controller.valueFormatter({ value, type, format }); - setPreviewResponse({ + controller.setPreviewResponse({ fields: [{ key: fieldName, value, formattedValue }], error: null, }); }, - [valueFormatter] + [controller, type, format] ); const updateCompositeFieldPreview = useCallback( @@ -359,7 +312,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro updatedFieldsInScript.push(fieldName); const [value] = values; - const formattedValue = valueFormatter(value); + const formattedValue = controller.valueFormatter({ value, type, format }); return { key: parentName @@ -375,12 +328,12 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro .sort((a, b) => a.key.localeCompare(b.key)); fieldPreview$.current.next(fields); - setPreviewResponse({ + controller.setPreviewResponse({ fields, error: null, }); }, - [valueFormatter, parentName, name, fieldPreview$, fieldName$] + [parentName, name, fieldPreview$, fieldName$, controller, type, format] ); const updatePreview = useCallback(async () => { @@ -437,7 +390,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro const { values, error } = response.data; if (error) { - setPreviewResponse({ + controller.setPreviewResponse({ fields: [ { key: name ?? '', @@ -474,6 +427,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro updateSingleFieldPreview, updateCompositeFieldPreview, currentDocIndex, + controller, ]); const reset = useCallback(() => { @@ -482,7 +436,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro previewCount.current = 0; controller.setDocuments([]); - setPreviewResponse({ fields: [], error: null }); + controller.setPreviewResponse({ fields: [], error: null }); setIsLoadingPreview(false); setIsFetchingDocument(false); }, [controller]); @@ -490,8 +444,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro const ctx = useMemo( () => ({ controller, - fields: previewResponse.fields, - error: previewResponse.error, fieldPreview$: fieldPreview$.current, isPreviewAvailable, isLoadingPreview, @@ -521,7 +473,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro [ controller, currentIdx, - previewResponse, fieldPreview$, fetchDocError, params, @@ -583,74 +534,70 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro * Whenever the name or the format changes we immediately update the preview */ useEffect(() => { - setPreviewResponse((prev) => { - const { fields } = prev; - - let updatedFields: Context['fields'] = fields.map((field) => { - let key = name ?? ''; - - if (type === 'composite') { - // restore initial key segement (the parent name), which was not returned - const { 1: fieldName } = field.key.split('.'); - key = `${name ?? ''}.${fieldName}`; - } + const { previewResponse: prev } = controller.state$.getValue(); + const { fields } = prev; - return { - ...field, - key, - }; - }); + let updatedFields: PreviewState['previewResponse']['fields'] = fields.map((field) => { + let key = name ?? ''; - // If the user has entered a name but not yet any script we will display - // the field in the preview with just the name - if (updatedFields.length === 0 && name !== null) { - updatedFields = [ - { key: name, value: undefined, formattedValue: undefined, type: undefined }, - ]; + if (type === 'composite') { + // restore initial key segement (the parent name), which was not returned + const { 1: fieldName } = field.key.split('.'); + key = `${name ?? ''}.${fieldName}`; } return { - ...prev, - fields: updatedFields, + ...field, + key, }; }); - }, [name, type, parentName]); + + // If the user has entered a name but not yet any script we will display + // the field in the preview with just the name + if (updatedFields.length === 0 && name !== null) { + updatedFields = [{ key: name, value: undefined, formattedValue: undefined, type: undefined }]; + } + + controller.setPreviewResponse({ + ...prev, + fields: updatedFields, + }); + }, [name, type, parentName, controller]); /** * Whenever the format changes we immediately update the preview */ useEffect(() => { - setPreviewResponse((prev) => { - const { fields } = prev; + const { previewResponse: prev } = controller.state$.getValue(); + const { fields } = prev; - return { - ...prev, - fields: fields.map((field) => { - const nextValue = - script === null && Boolean(document) - ? get(document?._source, name ?? '') ?? get(document?.fields, name ?? '') // When there is no script we try to read the value from _source/fields - : field?.value; + controller.setPreviewResponse({ + ...prev, + fields: fields.map((field) => { + const nextValue = + script === null && Boolean(document) + ? get(document?._source, name ?? '') ?? get(document?.fields, name ?? '') // When there is no script we try to read the value from _source/fields + : field?.value; - const formattedValue = valueFormatter(nextValue); + const formattedValue = controller.valueFormatter({ value: nextValue, type, format }); - return { - ...field, - value: nextValue, - formattedValue, - }; - }), - }; + return { + ...field, + value: nextValue, + formattedValue, + }; + }), }); - }, [name, script, document, valueFormatter]); + }, [name, script, document, controller, type, format]); useEffect(() => { if (script?.source === undefined) { // Whenever the source is not defined ("Set value" is toggled off or the // script is empty) we clear the error and update the params cache. lastExecutePainlessRequestParams.current.script = undefined; - setPreviewError(null); + controller.setPreviewError(null); } - }, [script?.source, setPreviewError]); + }, [script?.source, controller]); // Handle the validation state coming from the Painless DiagnosticAdapter // (see @kbn-monaco/src/painless/diagnostics_adapter.ts) @@ -677,16 +624,16 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro }), }, }; - setPreviewError(error); + controller.setPreviewError(error); // Make sure to update the lastExecutePainlessRequestParams cache so when the user updates // the script and fixes the syntax the "updatePreview()" will run lastExecutePainlessRequestParams.current.script = script?.source; } else { // Clear possible previous syntax error - clearPreviewError('PAINLESS_SYNTAX_ERROR'); + controller.clearPreviewError('PAINLESS_SYNTAX_ERROR'); } - }, [scriptEditorValidation, script?.source, setPreviewError, clearPreviewError]); + }, [scriptEditorValidation, script?.source, controller]); /** * Whenever updatePreview() changes (meaning whenever a param changes) diff --git a/src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts b/src/plugins/data_view_field_editor/public/components/preview/preview_controller.tsx similarity index 60% rename from src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts rename to src/plugins/data_view_field_editor/public/components/preview/preview_controller.tsx index b572827eac06dc..8e9f6156c7d7b9 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts +++ b/src/plugins/data_view_field_editor/public/components/preview/preview_controller.tsx @@ -9,13 +9,23 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { ISearchStart } from '@kbn/data-plugin/public'; import { BehaviorSubject } from 'rxjs'; +import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { renderToString } from 'react-dom/server'; +import React from 'react'; import { PreviewState } from './types'; import { BehaviorObservable } from '../../state_utils'; -import { EsDocument } from './types'; +import { EsDocument, ScriptErrorCodes, Params } from './types'; +import type { FieldFormatsStart } from '../../shared_imports'; + +export const defaultValueFormatter = (value: unknown) => { + const content = typeof value === 'object' ? JSON.stringify(value) : String(value) ?? '-'; + return renderToString(<>{content}); +}; interface PreviewControllerDependencies { dataView: DataView; search: ISearchStart; + fieldFormats: FieldFormatsStart; } const previewStateDefault: PreviewState = { @@ -30,12 +40,14 @@ const previewStateDefault: PreviewState = { documentSource: 'cluster', /** Keep track if the script painless syntax is being validated and if it is valid */ scriptEditorValidation: { isValidating: false, isValid: true, message: null }, + previewResponse: { fields: [], error: null }, }; export class PreviewController { - constructor({ dataView, search }: PreviewControllerDependencies) { + constructor({ dataView, search, fieldFormats }: PreviewControllerDependencies) { this.dataView = dataView; this.search = search; + this.fieldFormats = fieldFormats; this.internalState$ = new BehaviorSubject({ ...previewStateDefault, @@ -44,10 +56,13 @@ export class PreviewController { this.state$ = this.internalState$ as BehaviorObservable; } + // dependencies // @ts-ignore private dataView: DataView; // @ts-ignore private search: ISearchStart; + private fieldFormats: FieldFormatsStart; + private internalState$: BehaviorSubject; state$: BehaviorObservable; @@ -104,4 +119,52 @@ export class PreviewController { setCustomId = (customId?: string) => { this.updateState({ customId }); }; + + setPreviewError = (error: PreviewState['previewResponse']['error']) => { + this.updateState({ + previewResponse: { ...this.internalState$.getValue().previewResponse, error }, + }); + }; + + setPreviewResponse = (previewResponse: PreviewState['previewResponse']) => { + this.updateState({ previewResponse }); + }; + + clearPreviewError = (errorCode: ScriptErrorCodes) => { + const { previewResponse: prev } = this.internalState$.getValue(); + const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error; + this.updateState({ + previewResponse: { + ...prev, + error, + }, + }); + }; + + valueFormatter = ({ + value, + format, + type, + }: { + value: unknown; + format: Params['format']; + type: Params['type']; + }) => { + if (format?.id) { + const formatter = this.fieldFormats.getInstance(format.id, format.params); + if (formatter) { + return formatter.getConverterFor('html')(value) ?? JSON.stringify(value); + } + } + + if (type) { + const fieldType = castEsToKbnFieldTypeName(type); + const defaultFormatterForType = this.fieldFormats.getDefaultInstance(fieldType); + if (defaultFormatterForType) { + return defaultFormatterForType.getConverterFor('html')(value) ?? JSON.stringify(value); + } + } + + return defaultValueFormatter(value); + }; } diff --git a/src/plugins/data_view_field_editor/public/components/preview/types.ts b/src/plugins/data_view_field_editor/public/components/preview/types.ts index 347e0a709cf28a..b4280f54786ac0 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/types.ts +++ b/src/plugins/data_view_field_editor/public/components/preview/types.ts @@ -55,6 +55,11 @@ export interface PreviewState { isValid: boolean; message: string | null; }; + /** Response from the Painless _execute API */ + previewResponse: { + fields: FieldPreview[]; + error: PreviewError | null; + }; } export interface FetchDocError { @@ -108,9 +113,7 @@ export type ChangeSet = Record; export interface Context { controller: PreviewController; - fields: FieldPreview[]; fieldPreview$: BehaviorSubject; - error: PreviewError | null; fieldTypeInfo?: FieldTypeInfo[]; initialPreviewComplete: boolean; params: { From 11155329cc350c1b7a9cbaefcde34a802d803951 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Fri, 21 Apr 2023 16:01:43 -0700 Subject: [PATCH 15/65] [Security Solution][Exceptions] - Add exception list duplication options with and without expired items (#154991) ## Summary Adds the following: - Add the option to duplicate from the shared exception list management actions dropdowns - User can select to include exception items with expired TTL - User can select to not include exception items with expired TTL - Cypress tests added for both options --- .../__snapshots__/list_header.test.tsx.snap | 21 ++ .../src/list_header/index.tsx | 3 + .../src/list_header/list_header.test.tsx | 7 + .../__snapshots__/menu_items.test.tsx.snap | 43 ++- .../src/list_header/menu_items/index.tsx | 11 + .../menu_items/menu_items.test.tsx | 34 ++- .../src/translations.ts | 6 + .../index.mock.ts | 17 ++ .../index.test.ts | 66 +++++ .../index.ts | 34 +++ .../src/request/index.ts | 1 + .../src/typescript_types/index.ts | 23 +- .../tsconfig.json | 4 +- .../src/api/index.ts | 29 ++ .../src/use_api/index.ts | 34 ++- .../tsconfig.json | 1 + .../lists/public/exceptions/api.test.ts | 27 ++ .../public/exceptions/hooks/use_api.test.ts | 58 ++++ .../routes/duplicate_exception_list_route.ts | 94 +++++++ x-pack/plugins/lists/server/routes/index.ts | 1 + .../lists/server/routes/init_routes.ts | 2 + .../duplicate_exception_list.test.ts | 121 ++++++++ .../duplicate_exception_list.ts | 53 ++-- .../exception_lists/exception_list_client.ts | 10 +- .../exception_list_client_types.ts | 7 +- .../rules/bulk_actions/request_schema.test.ts | 5 +- .../api/rules/bulk_actions/request_schema.ts | 1 + .../rule_management/constants.ts | 1 + .../bulk_duplicate_rules.cy.ts | 132 +++++++++ .../indicator_match_rule.cy.ts | 4 +- .../alerts_table_flow/add_exception.cy.ts | 4 +- .../all_exception_lists_read_only.cy.ts | 9 +- .../manage_shared_exception_list.cy.ts | 264 ++++++++++++------ .../cypress/objects/exception.ts | 9 + .../cypress/screens/alerts_detection_rules.ts | 7 + .../cypress/screens/exceptions.ts | 11 +- .../cypress/screens/rule_details.ts | 6 + .../cypress/tasks/alerts_detection_rules.ts | 22 +- .../cypress/tasks/api_calls/exceptions.ts | 14 +- .../cypress/tasks/exceptions.ts | 16 ++ .../cypress/tasks/exceptions_table.ts | 47 +++- .../cypress/tasks/rule_details.ts | 7 + ...bulk_duplicate_exceptions_confirmation.tsx | 24 +- .../rules_table/bulk_actions/translations.tsx | 20 +- .../bulk_actions/use_bulk_actions.tsx | 8 +- .../rules_table/use_rules_table_actions.tsx | 8 +- .../rules/rule_actions_overflow/index.tsx | 8 +- .../exceptions_list_card/index.test.tsx | 109 ++++++++ .../components/exceptions_list_card/index.tsx | 31 +- .../index.test.tsx | 40 +++ .../index.tsx | 75 +++++ .../export_exceptions_list_modal/index.tsx | 50 ---- .../hooks/use_exceptions_list.card/index.tsx | 86 ++++-- .../hooks/use_list_detail_view/index.ts | 32 ++- .../pages/list_detail_view/index.tsx | 81 ++++-- .../exceptions/pages/shared_lists/index.tsx | 42 ++- .../translations/list_details_view.ts | 14 + .../exceptions/translations/shared_list.ts | 61 +++- .../api/rules/bulk_actions/route.ts | 3 + .../actions/duplicate_exceptions.test.ts | 163 +++++++++++ .../logic/actions/duplicate_exceptions.ts | 59 +++- .../translations/translations/fr-FR.json | 5 - .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - .../group10/perform_bulk_action.ts | 220 ++++++++++++++- .../tests/duplicate_exception_list.ts | 207 ++++++++++++++ .../security_and_spaces/tests/index.ts | 1 + 67 files changed, 2297 insertions(+), 326 deletions(-) create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.mock.ts create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.test.ts create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.ts create mode 100644 x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts create mode 100644 x-pack/plugins/lists/server/services/exception_lists/duplicate_exception_list.test.ts create mode 100644 x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_duplicate_rules.cy.ts create mode 100644 x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/exceptions/components/expired_exceptions_list_items_modal/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/exceptions/components/expired_exceptions_list_items_modal/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/exceptions/components/export_exceptions_list_modal/index.tsx create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.test.ts create mode 100644 x-pack/test/lists_api_integration/security_and_spaces/tests/duplicate_exception_list.ts diff --git a/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap b/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap index 871e3ea311cd62..b72c08b8d74ac1 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap +++ b/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap @@ -1876,6 +1876,27 @@ Object { data-test-subj="RightSideMenuItemsMenuActionsActionItem2" disabled="" type="button" + > + + + + Duplicate exception list + + + + + +
diff --git a/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts b/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts index 6d1f8c115fab37..e2688d804b3d2f 100644 --- a/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts +++ b/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts @@ -167,3 +167,17 @@ export const EXCEPTION_EXPORT_ERROR_DESCRIPTION = i18n.translate( defaultMessage: 'An error occurred exporting a list', } ); + +export const DUPLICATE_EXCEPTION_LIST = i18n.translate( + 'xpack.securitySolution.exceptionsTable.duplicateExceptionList', + { + defaultMessage: 'Duplicate exception list', + } +); + +export const EXCEPTION_DUPLICATE_ERROR_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptionsTable.duplicateListDescription', + { + defaultMessage: 'An error occurred duplicating a list', + } +); diff --git a/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts b/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts index 6ab1ca6df84644..eb182835773690 100644 --- a/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts +++ b/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts @@ -125,6 +125,19 @@ export const EXCEPTION_EXPORT_ERROR = i18n.translate( } ); +export const EXCEPTION_LIST_DUPLICATED_SUCCESSFULLY = (listName: string) => + i18n.translate('xpack.securitySolution.exceptions.list.duplicate_success', { + values: { listName }, + defaultMessage: 'Exception list "{listName}" duplicated successfully', + }); + +export const EXCEPTION_DUPLICATE_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.all.exceptions.duplicateError', + { + defaultMessage: 'Exception list duplication error', + } +); + export const EXCEPTION_DELETE_ERROR = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.all.exceptions.deleteError', { @@ -371,29 +384,59 @@ export const SORT_BY_CREATE_AT = i18n.translate( } ); -export const EXPORT_MODAL_CANCEL_BUTTON = i18n.translate( - 'xpack.securitySolution.exceptions.exportModalCancelButton', +export const EXPIRED_EXCEPTIONS_MODAL_CANCEL_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalCancelButton', { defaultMessage: 'Cancel', } ); -export const EXPORT_MODAL_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.exportModalTitle', +export const EXPIRED_EXCEPTIONS_MODAL_EXPORT_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalExportTitle', { - defaultMessage: 'Export exception list', + defaultMessage: 'Export exception list?', } ); -export const EXPORT_MODAL_INCLUDE_SWITCH_LABEL = i18n.translate( - 'xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel', +export const EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalDuplicateTitle', + { + defaultMessage: 'Duplicate exception list?', + } +); + +export const EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalIncludeDuplicateDescription', + { + defaultMessage: + 'You’re duplicating an exception list. Switch the toggle off to exclude expired exceptions.', + } +); + +export const EXPIRED_EXCEPTIONS_MODAL_EXPORT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalIncludeExportDescription', + { + defaultMessage: + 'You’re exporting an exception list. Switch the toggle off to exclude expired exceptions.', + } +); + +export const EXPIRED_EXCEPTIONS_MODAL_INCLUDE_SWITCH_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalIncludeSwitchLabel', { defaultMessage: 'Include expired exceptions', } ); -export const EXPORT_MODAL_CONFIRM_BUTTON = i18n.translate( - 'xpack.securitySolution.exceptions.exportModalConfirmButton', +export const EXPIRED_EXCEPTIONS_MODAL_CONFIRM_DUPLICATE_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalConfirmDuplicateButton', + { + defaultMessage: 'Duplicate', + } +); + +export const EXPIRED_EXCEPTIONS_MODAL_CONFIRM_EXPORT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalConfirmExportButton', { defaultMessage: 'Export', } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 27b9111b0434d5..974ea9cc7ecc53 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -432,8 +432,10 @@ export const performBulkActionRoute = ( } let shouldDuplicateExceptions = true; + let shouldDuplicateExpiredExceptions = true; if (body.duplicate !== undefined) { shouldDuplicateExceptions = body.duplicate.include_exceptions; + shouldDuplicateExpiredExceptions = body.duplicate.include_expired_exceptions; } const duplicateRuleToCreate = await duplicateRule({ @@ -449,6 +451,7 @@ export const performBulkActionRoute = ( ? await duplicateExceptions({ ruleId: rule.params.ruleId, exceptionLists: rule.params.exceptionsList, + includeExpiredExceptions: shouldDuplicateExpiredExceptions, exceptionsClient, }) : []; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.test.ts new file mode 100644 index 00000000000000..aeb593ec33fff8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { duplicateExceptions } from './duplicate_exceptions'; +import { getExceptionListClientMock } from '@kbn/lists-plugin/server/services/exception_lists/exception_list_client.mock'; +import type { List } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import type { ExceptionListClient } from '@kbn/lists-plugin/server'; +import { getDetectionsExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; + +jest.mock('uuid', () => ({ + v4: jest.fn(), +})); + +describe('duplicateExceptions', () => { + let exceptionsClient: ExceptionListClient; + + beforeAll(() => { + exceptionsClient = getExceptionListClientMock(); + exceptionsClient.duplicateExceptionListAndItems = jest.fn(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('returns empty array if no exceptions to duplicate', async () => { + const result = await duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [], + exceptionsClient, + includeExpiredExceptions: false, + }); + + expect(result).toEqual([]); + }); + + it('returns array referencing the same shared exception lists if no rule default exceptions included', async () => { + const sharedExceptionListReference: List = { + type: ExceptionListTypeEnum.DETECTION, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }; + + const result = await duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [sharedExceptionListReference], + exceptionsClient, + includeExpiredExceptions: false, + }); + + expect(exceptionsClient.duplicateExceptionListAndItems).not.toHaveBeenCalled(); + expect(result).toEqual([sharedExceptionListReference]); + }); + + it('duplicates rule default and shared exceptions', async () => { + const newDefaultRuleList = { + ...getDetectionsExceptionListSchemaMock(), + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'rule_default_list_dupe', + namespace_type: 'single', + id: '123-abc', + }; + + exceptionsClient.getExceptionList = jest.fn().mockResolvedValue({ + ...getDetectionsExceptionListSchemaMock(), + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'rule_default_list', + namespace_type: 'single', + id: '5678', + }); + exceptionsClient.duplicateExceptionListAndItems = jest + .fn() + .mockResolvedValue(newDefaultRuleList); + + const sharedExceptionListReference: List = { + type: ExceptionListTypeEnum.DETECTION, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }; + + const ruleDefaultListReference: List = { + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'rule_default_list', + namespace_type: 'single', + id: '5678', + }; + + const result = await duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [sharedExceptionListReference, ruleDefaultListReference], + exceptionsClient, + includeExpiredExceptions: false, + }); + + expect(result).toEqual([ + sharedExceptionListReference, + { + type: newDefaultRuleList.type, + namespace_type: newDefaultRuleList.namespace_type, + id: newDefaultRuleList.id, + list_id: newDefaultRuleList.list_id, + }, + ]); + }); + + it('throws error if rule default list to duplicate not found', async () => { + exceptionsClient.getExceptionList = jest.fn().mockResolvedValue(null); + + const ruleDefaultListReference: List = { + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }; + + await expect(() => + duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [ruleDefaultListReference], + exceptionsClient, + includeExpiredExceptions: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unable to duplicate rule default exceptions - unable to find their container with list_id: "my_list"]` + ); + }); + + it('throws error if list duplication returns null', async () => { + exceptionsClient.getExceptionList = jest.fn().mockResolvedValue({ + ...getDetectionsExceptionListSchemaMock(), + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }); + exceptionsClient.duplicateExceptionListAndItems = jest.fn().mockResolvedValue(null); + + const ruleDefaultListReference: List = { + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }; + + await expect(() => + duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [ruleDefaultListReference], + exceptionsClient, + includeExpiredExceptions: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unable to duplicate rule default exception items for rule_id: rule_123]` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.ts index 496a91ba559634..82f13adc4534c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.ts @@ -5,23 +5,42 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; + import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { RuleParams } from '../../../rule_schema'; +const ERROR_DUPLICATING = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.cloneExceptions.errorDuplicatingList', + { + defaultMessage: + 'Unable to duplicate rule default exceptions - unable to find their container with list_id:', + } +); + +const ERROR_DUPLICATING_ITEMS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.cloneExceptions.errorDuplicatingListItems', + { + defaultMessage: 'Unable to duplicate rule default exception items for rule_id:', + } +); + interface DuplicateExceptionsParams { ruleId: RuleParams['ruleId']; exceptionLists: RuleParams['exceptionsList']; exceptionsClient: ExceptionListClient | undefined; + includeExpiredExceptions: boolean; } export const duplicateExceptions = async ({ ruleId, exceptionLists, exceptionsClient, + includeExpiredExceptions, }: DuplicateExceptionsParams): Promise => { - if (exceptionLists == null) { + if (exceptionLists == null || !exceptionLists.length) { return []; } @@ -37,24 +56,36 @@ export const duplicateExceptions = async ({ // For rule_default list (exceptions that live only on a single rule), we need // to create a new rule_default list to assign to duplicated rule if (ruleDefaultList != null && exceptionsClient != null) { - const ruleDefaultExceptionList = await exceptionsClient.duplicateExceptionListAndItems({ + // fetch list container + const listToDuplicate = await exceptionsClient.getExceptionList({ + id: undefined, listId: ruleDefaultList.list_id, namespaceType: ruleDefaultList.namespace_type, }); - if (ruleDefaultExceptionList == null) { - throw new Error(`Unable to duplicate rule default exception items for rule_id: ${ruleId}`); - } + if (listToDuplicate == null) { + throw new Error(`${ERROR_DUPLICATING} "${ruleDefaultList.list_id}"`); + } else { + const ruleDefaultExceptionList = await exceptionsClient.duplicateExceptionListAndItems({ + list: listToDuplicate, + namespaceType: ruleDefaultList.namespace_type, + includeExpiredExceptions, + }); - return [ - ...sharedLists, - { - id: ruleDefaultExceptionList.id, - list_id: ruleDefaultExceptionList.list_id, - namespace_type: ruleDefaultExceptionList.namespace_type, - type: ruleDefaultExceptionList.type, - }, - ]; + if (ruleDefaultExceptionList == null) { + throw new Error(`${ERROR_DUPLICATING_ITEMS} ${ruleId}`); + } + + return [ + ...sharedLists, + { + id: ruleDefaultExceptionList.id, + list_id: ruleDefaultExceptionList.list_id, + namespace_type: ruleDefaultExceptionList.namespace_type, + type: ruleDefaultExceptionList.type, + }, + ]; + } } // If no rule_default list exists, we can just return diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9a22d99e537025..dbc92e04c393f7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27824,7 +27824,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.successToastDescription": "Désactivation réussie de {totalRules, plural, =1 {{totalRules} règle} other {{totalRules} règles}}", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastDescription": "Impossible de dupliquer {rulesCount, plural, =1 {# règle} other {# règles}}.", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalBody": "Vous dupliquez {rulesCount, plural, one {# règle sélectionnée} other {# règles sélectionnées}}. Veuillez choisir comment dupliquer les exceptions existantes", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalTitle": "Dupliquer {rulesCount, plural, one {la règle} other {les règles}} avec les exceptions ?", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.with": "Dupliquer {rulesCount, plural, one {la règle et ses} other {les règles et leurs}} exceptions", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.without": "Dupliquer uniquement {rulesCount, plural, one {la règle} other {les règles}}", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "Duplication réussie de {totalRules, plural, =1 {{totalRules} règle} other {{totalRules} règles}}", @@ -30891,10 +30890,6 @@ "xpack.securitySolution.exceptions.exceptionListsCloseImportFlyout": "Fermer", "xpack.securitySolution.exceptions.exceptionListsFilePickerPrompt": "Sélectionner ou glisser-déposer plusieurs fichiers", "xpack.securitySolution.exceptions.exceptionListsImportButton": "Importer la liste", - "xpack.securitySolution.exceptions.exportModalCancelButton": "Annuler", - "xpack.securitySolution.exceptions.exportModalConfirmButton": "Exporter", - "xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel": "Inclure les exceptions ayant expiré", - "xpack.securitySolution.exceptions.exportModalTitle": "Exporter la liste d'exceptions", "xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions", "xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle": "Erreur lors de la récupération des références d'exceptions", "xpack.securitySolution.exceptions.list.exception.item.card.delete.label": "Supprimer une exception à une règle", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 083d8d827afcf5..22cfeb42e69d82 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27804,7 +27804,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.successToastDescription": "{totalRules, plural, =1 {{totalRules}個のルール} other {{totalRules}個のルール}}が正常に無効にされました", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastDescription": "{rulesCount, plural, =1 {#個のルール} other {#個のルール}}を複製できませんでした。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalBody": "{rulesCount, plural, other {#個の選択したルール}}を複製しています。既存の例外を複製する方法を選択してください", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalTitle": "{rulesCount, plural, other {ルール}}と例外を複製しますか?", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.with": "{rulesCount, plural, other {ルール}}と例外を複製", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.without": "{rulesCount, plural, other {ルール}}のみを複製", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "{totalRules, plural, =1 {{totalRules}個のルール} other {{totalRules}個のルール}}が正常に複製されました", @@ -30870,10 +30869,6 @@ "xpack.securitySolution.exceptions.exceptionListsCloseImportFlyout": "閉じる", "xpack.securitySolution.exceptions.exceptionListsFilePickerPrompt": "複数のファイルを選択するかドラッグしてください", "xpack.securitySolution.exceptions.exceptionListsImportButton": "リストをインポート", - "xpack.securitySolution.exceptions.exportModalCancelButton": "キャンセル", - "xpack.securitySolution.exceptions.exportModalConfirmButton": "エクスポート", - "xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel": "有効期限切れの例外を含める", - "xpack.securitySolution.exceptions.exportModalTitle": "例外リストのエクスポート", "xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー", "xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle": "例外参照の取得エラー", "xpack.securitySolution.exceptions.list.exception.item.card.delete.label": "ルール例外の削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c4d28406b874f1..67bf31893d10d6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27819,7 +27819,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.successToastDescription": "已成功禁用 {totalRules, plural, other {{totalRules} 个规则}}", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastDescription": "无法复制 {rulesCount, plural, other {# 个规则}}。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalBody": "您正在复制 {rulesCount, plural, other {# 个选定规则}},请选择您希望如何复制现有例外", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalTitle": "复制存在例外的{rulesCount, plural, other {规则}}?", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.with": "复制{rulesCount, plural, other {规则}}及其例外", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.without": "仅复制{rulesCount, plural, other {规则}}", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "已成功复制 {totalRules, plural, other {{totalRules} 个规则}}", @@ -30886,10 +30885,6 @@ "xpack.securitySolution.exceptions.exceptionListsCloseImportFlyout": "关闭", "xpack.securitySolution.exceptions.exceptionListsFilePickerPrompt": "选择或拖放多个文件", "xpack.securitySolution.exceptions.exceptionListsImportButton": "导入列表", - "xpack.securitySolution.exceptions.exportModalCancelButton": "取消", - "xpack.securitySolution.exceptions.exportModalConfirmButton": "导出", - "xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel": "包括已过期例外", - "xpack.securitySolution.exceptions.exportModalTitle": "导出例外列表", "xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错", "xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle": "提取例外引用时出错", "xpack.securitySolution.exceptions.list.exception.item.card.delete.label": "删除规则例外", diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index c77ac5f142f92d..e23db2120eb7e5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -17,7 +17,10 @@ import { BulkActionType, BulkActionEditType, } from '@kbn/security-solution-plugin/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; +import { deleteAllExceptions } from '../../../lists_api_integration/utils'; import { binaryToString, createLegacyRuleAction, @@ -35,6 +38,7 @@ import { removeServerGeneratedProperties, waitForRuleSuccess, } from '../../utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -416,15 +420,225 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.duplicate, - duplicate: { include_exceptions: false }, + duplicate: { include_exceptions: false, include_expired_exceptions: false }, + }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + + // Check that the duplicated rule is returned with the response + expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + + // Check that the updates have been persisted + const { body: rulesResponse } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(rulesResponse.total).to.eql(2); + }); + + it('should duplicate rules with exceptions - expired exceptions included', async () => { + await deleteAllExceptions(supertest, log); + + const expiredDate = new Date(Date.now() - 1000000).toISOString(); + + // create an exception list + const { body: exceptionList } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + // create an exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate }) + .expect(200); + + const ruleId = 'ruleId'; + const ruleToDuplicate = { + ...getSimpleRule(ruleId), + exceptions_list: [ + { + type: exceptionList.type, + list_id: exceptionList.list_id, + id: exceptionList.id, + namespace_type: exceptionList.namespace_type, + }, + ], + }; + const newRule = await createRule(supertest, log, ruleToDuplicate); + + // add an exception item to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${newRule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .send({ + items: [ + { + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: 'Sample exception item', + type: 'simple', + expire_time: expiredDate, + }, + ], + }) + .expect(200); + + const { body } = await postBulkAction() + .send({ + query: '', + action: BulkActionType.duplicate, + duplicate: { include_exceptions: true, include_expired_exceptions: true }, + }) + .expect(200); + + const { body: foundItems } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${body.attributes.results.created[0].exceptions_list[1].list_id}` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Item should have been duplicated, even if expired + expect(foundItems.total).to.eql(1); + + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + + // Check that the duplicated rule is returned with the response + expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + + // Check that the exceptions are duplicated + expect(body.attributes.results.created[0].exceptions_list).to.eql([ + { + type: exceptionList.type, + list_id: exceptionList.list_id, + id: exceptionList.id, + namespace_type: exceptionList.namespace_type, + }, + { + id: body.attributes.results.created[0].exceptions_list[1].id, + list_id: body.attributes.results.created[0].exceptions_list[1].list_id, + namespace_type: 'single', + type: 'rule_default', + }, + ]); + + // Check that the updates have been persisted + const { body: rulesResponse } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(rulesResponse.total).to.eql(2); + }); + + it('should duplicate rules with exceptions - expired exceptions excluded', async () => { + await deleteAllExceptions(supertest, log); + + const expiredDate = new Date(Date.now() - 1000000).toISOString(); + + // create an exception list + const { body: exceptionList } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + // create an exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate }) + .expect(200); + + const ruleId = 'ruleId'; + const ruleToDuplicate = { + ...getSimpleRule(ruleId), + exceptions_list: [ + { + type: exceptionList.type, + list_id: exceptionList.list_id, + id: exceptionList.id, + namespace_type: exceptionList.namespace_type, + }, + ], + }; + const newRule = await createRule(supertest, log, ruleToDuplicate); + + // add an exception item to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${newRule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .send({ + items: [ + { + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: 'Sample exception item', + type: 'simple', + expire_time: expiredDate, + }, + ], + }) + .expect(200); + + const { body } = await postBulkAction() + .send({ + query: '', + action: BulkActionType.duplicate, + duplicate: { include_exceptions: true, include_expired_exceptions: false }, }) .expect(200); + const { body: foundItems } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${body.attributes.results.created[0].exceptions_list[1].list_id}` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Item should NOT have been duplicated, since it is expired + expect(foundItems.total).to.eql(0); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the duplicated rule is returned with the response expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + // Check that the exceptions are duplicted + expect(body.attributes.results.created[0].exceptions_list).to.eql([ + { + type: exceptionList.type, + list_id: exceptionList.list_id, + id: exceptionList.id, + namespace_type: exceptionList.namespace_type, + }, + { + id: body.attributes.results.created[0].exceptions_list[1].id, + list_id: body.attributes.results.created[0].exceptions_list[1].list_id, + namespace_type: 'single', + type: 'rule_default', + }, + ]); + // Check that the updates have been persisted const { body: rulesResponse } = await supertest .get(`${DETECTION_ENGINE_RULES_URL}/_find`) @@ -462,7 +676,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.duplicate, - duplicate: { include_exceptions: false }, + duplicate: { include_exceptions: false, include_expired_exceptions: false }, }) .expect(200); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/duplicate_exception_list.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/duplicate_exception_list.ts new file mode 100644 index 00000000000000..54e7a89042b02a --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/duplicate_exception_list.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { + ENDPOINT_LIST_URL, + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '@kbn/securitysolution-list-constants'; +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; +import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const log = getService('log'); + + describe('duplicate_exception_lists', () => { + afterEach(async () => { + await deleteAllExceptions(supertest, log); + }); + + it('should duplicate a list with no exception items', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + const { body } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }&namespace_type=single&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql({ + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + type: 'detection', + list_id: body.list_id, + name: `${getCreateExceptionListDetectionSchemaMock().name} [Duplicate]`, + }); + }); + + it('should duplicate a list and its items', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMock(), + list_id: getCreateExceptionListDetectionSchemaMock().list_id, + }) + .expect(200); + + const { body: listBody } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }&namespace_type=single&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const listBodyToCompare = removeExceptionListServerGeneratedProperties(listBody); + expect(listBodyToCompare).to.eql({ + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + type: 'detection', + list_id: listBody.list_id, + name: `${getCreateExceptionListDetectionSchemaMock().name} [Duplicate]`, + }); + + expect(body.total).to.eql(1); + }); + + it('should duplicate a list with expired exception items', async () => { + const expiredDate = new Date(Date.now() - 1000000).toISOString(); + + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate }) + .expect(200); + + const { body: listBody } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }&namespace_type=single&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.total).to.eql(1); + }); + + it('should duplicate a list and EXCLUDE expired exception items when "include_expired_exceptions" set to "false"', async () => { + const expiredDate = new Date(Date.now() - 1000000).toISOString(); + + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMock(), + list_id: getCreateExceptionListDetectionSchemaMock().list_id, + expire_time: expiredDate, + }) + .expect(200); + + const { body: listBody } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }&namespace_type=single&include_expired_exceptions=false` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.total).to.eql(0); + }); + + describe('error states', () => { + it('should cause a 409 if list does not exist', async () => { + const { body } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=exception_list_id&namespace_type=agnostic&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'exception list id: "exception_list_id" does not exist', + status_code: 404, + }); + }); + + it('should cause a 405 if trying to duplicate a reserved exception list type', async () => { + // create an exception list + await supertest.post(ENDPOINT_LIST_URL).set('kbn-xsrf', 'true').expect(200); + + const { body } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=endpoint_list&namespace_type=agnostic&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(405); + + expect(body).to.eql({ + message: + 'unable to duplicate exception list with list_id: endpoint_list - action not allowed', + status_code: 405, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts index bfce9ac8267e7b..02b63c732d229d 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts @@ -19,6 +19,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./update_list_items')); loadTestFile(require.resolve('./delete_lists')); loadTestFile(require.resolve('./delete_list_items')); + loadTestFile(require.resolve('./duplicate_exception_list')); loadTestFile(require.resolve('./find_lists')); loadTestFile(require.resolve('./find_list_items')); loadTestFile(require.resolve('./find_lists_by_size')); From 350bd3eb6de7512a847b4751d553f394a5c4f74d Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Fri, 21 Apr 2023 18:14:06 -0500 Subject: [PATCH 16/65] [RAM] Add query conditional action filter to Security Solution UI (#154680) ## Summary Closes #152611 Screenshot 2023-04-10 at 2 09 29 PM ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) --- .../src/actions/index.ts | 29 +++++- .../query_string_input/query_bar_top_row.tsx | 2 + .../public/search_bar/search_bar.tsx | 3 + x-pack/plugins/alerting/common/rule.ts | 12 ++- x-pack/plugins/alerting/server/alert/alert.ts | 6 +- .../server/routes/create_rule.test.ts | 7 +- .../alerting/server/routes/get_rule.test.ts | 1 + .../server/routes/lib/actions_schema.ts | 11 ++- .../server/routes/lib/rewrite_actions.test.ts | 5 +- .../server/routes/lib/rewrite_rule.test.ts | 2 +- .../server/routes/update_rule.test.ts | 4 +- .../lib/add_generated_action_values.test.ts | 17 +++- .../lib/add_generated_action_values.ts | 21 ++-- .../rules_client/lib/validate_actions.test.ts | 8 +- .../rules_client/lib/validate_actions.ts | 1 - .../rules_client/tests/bulk_edit.test.ts | 14 +-- .../server/rules_client/tests/create.test.ts | 2 +- .../task_runner/execution_handler.test.ts | 10 +- x-pack/plugins/alerting/server/types.ts | 6 +- .../create_get_summarized_alerts_fn.test.ts | 25 +++-- .../utils/create_get_summarized_alerts_fn.ts | 8 +- .../rules/rule_actions_field/index.tsx | 16 +++- .../public/application/lib/rule_api/create.ts | 2 +- .../public/application/lib/rule_api/update.ts | 2 +- .../action_alerts_filter_query.tsx | 96 +++++++++++++++++++ .../action_alerts_filter_timeframe.test.tsx | 4 +- .../action_alerts_filter_timeframe.tsx | 12 +-- .../action_form.test.tsx | 2 +- .../action_type_form.tsx | 8 +- .../alerts_search_bar/alerts_search_bar.tsx | 17 +++- .../sections/alerts_search_bar/types.ts | 8 ++ .../sections/rule_form/rule_reducer.ts | 17 +++- .../group2/tests/alerting/alerts.ts | 7 +- 33 files changed, 294 insertions(+), 91 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_query.tsx diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts index b173d67c9f005c..ab6df3bdacc2aa 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts @@ -29,18 +29,41 @@ export const RuleActionUuid = NonEmptyString; export type RuleActionParams = t.TypeOf; export const RuleActionParams = saved_object_attributes; -export const RuleActionAlertsFilter = t.strict({ +export const RuleActionAlertsFilter = t.partial({ query: t.union([ - t.null, + t.undefined, t.intersection([ t.strict({ kql: t.string, + filters: t.array( + t.intersection([ + t.type({ + meta: t.partial({ + alias: t.union([t.string, t.null]), + disabled: t.boolean, + negate: t.boolean, + controlledBy: t.string, + group: t.string, + index: t.string, + isMultiIndex: t.boolean, + type: t.string, + key: t.string, + params: t.any, + value: t.string, + }), + }), + t.partial({ + $state: t.type({ store: t.any }), + query: t.record(t.string, t.any), + }), + ]) + ), }), t.partial({ dsl: t.string }), ]), ]), timeframe: t.union([ - t.null, + t.undefined, t.strict({ timezone: t.string, days: t.array( diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 8ca32108a25552..aecd588673e09a 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -120,6 +120,7 @@ export interface QueryBarTopRowProps isScreenshotMode?: boolean; onTextLangQuerySubmit: (query?: Query | AggregateQuery) => void; onTextLangQueryChange: (query: AggregateQuery) => void; + submitOnBlur?: boolean; } export const SharingMetaFields = React.memo(function SharingMetaFields({ @@ -556,6 +557,7 @@ export const QueryBarTopRow = React.memo( size={props.suggestionsSize} isDisabled={props.isDisabled} appName={appName} + submitOnBlur={props.submitOnBlur} deps={{ unifiedSearch, data, diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index 0a83a45de1d035..3326d81e29109f 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -105,6 +105,8 @@ export interface SearchBarOwnProps { * Disables all inputs and interactive elements, */ isDisabled?: boolean; + + submitOnBlur?: boolean; } export type SearchBarProps = SearchBarOwnProps & @@ -585,6 +587,7 @@ class SearchBarUI extends C isScreenshotMode={this.props.isScreenshotMode} onTextLangQuerySubmit={this.onTextLangQuerySubmit} onTextLangQueryChange={this.onTextLangQueryChange} + submitOnBlur={this.props.submitOnBlur} />
); diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 0bac3fd995a279..f58324f1d09ee2 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -10,7 +10,7 @@ import type { SavedObjectAttributes, SavedObjectsResolveResponse, } from '@kbn/core/server'; -import type { KueryNode } from '@kbn/es-query'; +import type { Filter, KueryNode } from '@kbn/es-query'; import { RuleNotifyWhenType } from './rule_notify_when_type'; import { RuleSnooze } from './rule_snooze_type'; @@ -94,11 +94,12 @@ export interface AlertsFilterTimeframe extends SavedObjectAttributes { } export interface AlertsFilter extends SavedObjectAttributes { - query: null | { + query?: { kql: string; + filters: Filter[]; dsl?: string; // This fields is generated in the code by using "kql", therefore it's not optional but defined as optional to avoid modifying a lot of files in different plugins }; - timeframe: null | AlertsFilterTimeframe; + timeframe?: AlertsFilterTimeframe; } export type RuleActionAlertsFilterProperty = AlertsFilterTimeframe | RuleActionParam; @@ -191,10 +192,11 @@ export interface Rule { } export interface SanitizedAlertsFilter extends AlertsFilter { - query: null | { + query?: { kql: string; + filters: Filter[]; }; - timeframe: null | AlertsFilterTimeframe; + timeframe?: AlertsFilterTimeframe; } export type SanitizedRuleAction = Omit & { diff --git a/x-pack/plugins/alerting/server/alert/alert.ts b/x-pack/plugins/alerting/server/alert/alert.ts index 1fff89d527d5a9..36877db2726c6b 100644 --- a/x-pack/plugins/alerting/server/alert/alert.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -7,7 +7,7 @@ import { v4 as uuidV4 } from 'uuid'; import { get, isEmpty } from 'lodash'; -import { ALERT_INSTANCE_ID } from '@kbn/rule-data-utils'; +import { ALERT_INSTANCE_ID, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import { CombinedSummarizedAlerts } from '../types'; import { AlertInstanceMeta, @@ -277,7 +277,9 @@ export class Alert< } return !summarizedAlerts.all.data.some( - (alert) => get(alert, ALERT_INSTANCE_ID) === this.getId() + (alert) => + get(alert, ALERT_INSTANCE_ID) === this.getId() || + get(alert, ALERT_RULE_UUID) === this.getId() ); } } diff --git a/x-pack/plugins/alerting/server/routes/create_rule.test.ts b/x-pack/plugins/alerting/server/routes/create_rule.test.ts index 4285c5a986b2b0..edd1d10d062366 100644 --- a/x-pack/plugins/alerting/server/routes/create_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/create_rule.test.ts @@ -56,6 +56,7 @@ describe('createRuleRoute', () => { query: { kql: 'name:test', dsl: '{"must": {"term": { "name": "test" }}}', + filters: [], }, timeframe: { days: [1], @@ -92,7 +93,7 @@ describe('createRuleRoute', () => { id: mockedAlert.actions[0].id, params: mockedAlert.actions[0].params, alerts_filter: { - query: { kql: mockedAlert.actions[0].alertsFilter!.query!.kql }, + query: { kql: mockedAlert.actions[0].alertsFilter!.query!.kql, filters: [] }, timeframe: mockedAlert.actions[0].alertsFilter?.timeframe!, }, }, @@ -167,6 +168,7 @@ describe('createRuleRoute', () => { Object { "alertsFilter": Object { "query": Object { + "filters": Array [], "kql": "name:test", }, "timeframe": Object { @@ -263,6 +265,7 @@ describe('createRuleRoute', () => { Object { "alertsFilter": Object { "query": Object { + "filters": Array [], "kql": "name:test", }, "timeframe": Object { @@ -360,6 +363,7 @@ describe('createRuleRoute', () => { Object { "alertsFilter": Object { "query": Object { + "filters": Array [], "kql": "name:test", }, "timeframe": Object { @@ -457,6 +461,7 @@ describe('createRuleRoute', () => { Object { "alertsFilter": Object { "query": Object { + "filters": Array [], "kql": "name:test", }, "timeframe": Object { diff --git a/x-pack/plugins/alerting/server/routes/get_rule.test.ts b/x-pack/plugins/alerting/server/routes/get_rule.test.ts index 17481e538ed86c..a672de9cbf320d 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.test.ts @@ -49,6 +49,7 @@ describe('getRuleRoute', () => { query: { kql: 'name:test', dsl: '{"must": {"term": { "name": "test" }}}', + filters: [], }, timeframe: { days: [1], diff --git a/x-pack/plugins/alerting/server/routes/lib/actions_schema.ts b/x-pack/plugins/alerting/server/routes/lib/actions_schema.ts index 3e561168aee48f..9d6f89e070c3a1 100644 --- a/x-pack/plugins/alerting/server/routes/lib/actions_schema.ts +++ b/x-pack/plugins/alerting/server/routes/lib/actions_schema.ts @@ -29,13 +29,20 @@ export const actionsSchema = schema.arrayOf( uuid: schema.maybe(schema.string()), alerts_filter: schema.maybe( schema.object({ - query: schema.nullable( + query: schema.maybe( schema.object({ kql: schema.string(), + filters: schema.arrayOf( + schema.object({ + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + meta: schema.recordOf(schema.string(), schema.any()), + state$: schema.maybe(schema.object({ store: schema.string() })), + }) + ), dsl: schema.maybe(schema.string()), }) ), - timeframe: schema.nullable( + timeframe: schema.maybe( schema.object({ days: schema.arrayOf( schema.oneOf([ diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_actions.test.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_actions.test.ts index b79e5a91ff3815..61dc9282bbfa12 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_actions.test.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_actions.test.ts @@ -27,6 +27,7 @@ describe('rewrite Actions', () => { query: { kql: 'test:1s', dsl: '{test:1}', + filters: [], }, timeframe: { days: [1, 2, 3], @@ -42,7 +43,7 @@ describe('rewrite Actions', () => { ).toEqual([ { alerts_filter: { - query: { dsl: '{test:1}', kql: 'test:1s' }, + query: { dsl: '{test:1}', kql: 'test:1s', filters: [] }, timeframe: { days: [1, 2, 3], hours: { end: '15:00', start: '00:00' }, @@ -77,6 +78,7 @@ describe('rewrite Actions', () => { query: { kql: 'test:1s', dsl: '{test:1}', + filters: [], }, timeframe: { days: [1, 2, 3], @@ -104,6 +106,7 @@ describe('rewrite Actions', () => { query: { kql: 'test:1s', dsl: '{test:1}', + filters: [], }, timeframe: { days: [1, 2, 3], diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.test.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.test.ts index d5d0ba1739355d..7a348e583ac6c4 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.test.ts @@ -43,7 +43,7 @@ const sampleRule: SanitizedRule & { activeSnoozes?: string[] } = notifyWhen: 'onThrottleInterval', throttle: '1m', }, - alertsFilter: { timeframe: null, query: { kql: 'test:1', dsl: '{}' } }, + alertsFilter: { query: { kql: 'test:1', dsl: '{}', filters: [] } }, }, ], scheduledTaskId: 'xyz456', diff --git a/x-pack/plugins/alerting/server/routes/update_rule.test.ts b/x-pack/plugins/alerting/server/routes/update_rule.test.ts index 4dfe1c00145e86..5a4b3a19c0d7ce 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.test.ts @@ -53,8 +53,8 @@ describe('updateRuleRoute', () => { query: { kql: 'name:test', dsl: '{"must": {"term": { "name": "test" }}}', + filters: [], }, - timeframe: null, }, }, ], @@ -123,9 +123,9 @@ describe('updateRuleRoute', () => { "alertsFilter": Object { "query": Object { "dsl": "{\\"must\\": {\\"term\\": { \\"name\\": \\"test\\" }}}", + "filters": Array [], "kql": "name:test", }, - "timeframe": null, }, "group": "default", "id": "2", diff --git a/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.test.ts index 7ee9d323c76275..6605d9c22a72bd 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.test.ts @@ -24,7 +24,15 @@ describe('addGeneratedActionValues()', () => { throttle: null, }, alertsFilter: { - query: { kql: 'test:testValue' }, + query: { + kql: 'test:testValue', + filters: [ + { + meta: { key: 'foo', params: { query: 'bar' } }, + query: { match_phrase: { foo: 'bar ' } }, + }, + ], + }, timeframe: { days: [1, 2], hours: { start: '08:00', end: '17:00' }, @@ -41,14 +49,17 @@ describe('addGeneratedActionValues()', () => { test('adds DSL', async () => { const actionWithGeneratedValues = addGeneratedActionValues([mockAction]); expect(actionWithGeneratedValues[0].alertsFilter?.query?.dsl).toBe( - '{"bool":{"should":[{"match":{"test":"testValue"}}],"minimum_should_match":1}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"test":"testValue"}}],"minimum_should_match":1}},{"match_phrase":{"foo":"bar "}}],"should":[],"must_not":[]}}' ); }); test('throws error if KQL is not valid', async () => { expect(() => addGeneratedActionValues([ - { ...mockAction, alertsFilter: { query: { kql: 'foo:bar:1' }, timeframe: null } }, + { + ...mockAction, + alertsFilter: { query: { kql: 'foo:bar:1', filters: [] } }, + }, ]) ).toThrowErrorMatchingInlineSnapshot('"Error creating DSL query: invalid KQL"'); }); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.ts b/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.ts index c3cd7216985945..71cbf01ea1a4e6 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.ts @@ -6,7 +6,7 @@ */ import { v4 } from 'uuid'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { buildEsQuery, Filter } from '@kbn/es-query'; import Boom from '@hapi/boom'; import { NormalizedAlertAction, NormalizedAlertActionWithGeneratedValues } from '..'; @@ -14,9 +14,11 @@ export function addGeneratedActionValues( actions: NormalizedAlertAction[] = [] ): NormalizedAlertActionWithGeneratedValues[] { return actions.map(({ uuid, alertsFilter, ...action }) => { - const generateDSL = (kql: string) => { + const generateDSL = (kql: string, filters: Filter[]) => { try { - return JSON.stringify(toElasticsearchQuery(fromKueryExpression(kql))); + return JSON.stringify( + buildEsQuery(undefined, [{ query: kql, language: 'kuery' }], filters) + ); } catch (e) { throw Boom.badRequest(`Error creating DSL query: invalid KQL`); } @@ -29,13 +31,12 @@ export function addGeneratedActionValues( ? { alertsFilter: { ...alertsFilter, - timeframe: alertsFilter.timeframe || null, - query: !alertsFilter.query - ? null - : { - kql: alertsFilter.query.kql, - dsl: generateDSL(alertsFilter.query.kql), - }, + query: alertsFilter.query + ? { + ...alertsFilter.query, + dsl: generateDSL(alertsFilter.query.kql, alertsFilter.query.filters), + } + : undefined, }, } : {}), diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts index 679f79d477c864..229c009df3eecc 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts @@ -45,7 +45,7 @@ describe('validateActions', () => { throttle: null, }, alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, timeframe: { days: [1], hours: { start: '10:00', end: '17:00' }, timezone: 'UTC' }, }, }, @@ -200,7 +200,7 @@ describe('validateActions', () => { { ...data.actions[0], alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, timeframe: { days: [1], hours: { start: '30:00', end: '17:00' }, timezone: 'UTC' }, }, }, @@ -237,7 +237,7 @@ describe('validateActions', () => { { ...data.actions[0], alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, // @ts-ignore timeframe: { days: [1], hours: { start: '10:00', end: '17:00' } }, }, @@ -261,7 +261,7 @@ describe('validateActions', () => { { ...data.actions[0], alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, timeframe: { // @ts-ignore days: [0, 8], diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts index 357f624a815cd8..b86e30556f090c 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts @@ -154,7 +154,6 @@ export async function validateActions( ) { actionWithInvalidTimeframe.push(action); } - // alertsFilter time range filter's start time can't be before end time if (alertsFilter.timeframe.hours) { if ( validateHours(alertsFilter.timeframe.hours.start) || diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts index 47b38cce094bac..3598ad31fd390c 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts @@ -706,7 +706,8 @@ describe('bulkEdit()', () => { alertsFilter: { query: { kql: 'name:test', - dsl: '{"bool":{"should":[{"match":{"name":"test"}}],"minimum_should_match":1}}', + dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"name":"test"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}', + filters: [], }, timeframe: { days: [1], @@ -725,7 +726,7 @@ describe('bulkEdit()', () => { id: '2', params: {}, uuid: '222', - alertsFilter: { query: { kql: 'test:1', dsl: 'test' } }, + alertsFilter: { query: { kql: 'test:1', dsl: 'test', filters: [] } }, }; unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ @@ -744,8 +745,7 @@ describe('bulkEdit()', () => { actionRef: 'action_1', uuid: '222', alertsFilter: { - query: { kql: 'test:1', dsl: 'test' }, - timeframe: null, + query: { kql: 'test:1', dsl: 'test', filters: [] }, }, }, ], @@ -802,10 +802,10 @@ describe('bulkEdit()', () => { uuid: '222', alertsFilter: { query: { - dsl: '{"bool":{"should":[{"match":{"test":"1"}}],"minimum_should_match":1}}', + dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"test":"1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}', kql: 'test:1', + filters: [], }, - timeframe: null, }, }, ], @@ -835,8 +835,8 @@ describe('bulkEdit()', () => { query: { dsl: 'test', kql: 'test:1', + filters: [], }, - timeframe: null, }, }, ], diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index d44d69a6e64bfb..aedf130e1f846f 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -3343,7 +3343,7 @@ describe('create()', () => { throttle: null, }, alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, }, }, ], diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts index 3aba0142260422..03e1ba5976a622 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts @@ -947,7 +947,7 @@ describe('Execution Handler', () => { message: 'New: {{alerts.new.count}} Ongoing: {{alerts.ongoing.count}} Recovered: {{alerts.recovered.count}}', }, - alertsFilter: { query: { kql: 'test:1', dsl: '{}' } }, + alertsFilter: { query: { kql: 'test:1', dsl: '{}', filters: [] } }, }, ], }, @@ -1332,7 +1332,7 @@ describe('Execution Handler', () => { 'New: {{alerts.new.count}} Ongoing: {{alerts.ongoing.count}} Recovered: {{alerts.recovered.count}}', }, alertsFilter: { - query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}' }, + query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] }, }, }, ], @@ -1351,7 +1351,7 @@ describe('Execution Handler', () => { spaceId: 'test1', excludedAlertInstanceIds: ['foo'], alertsFilter: { - query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}' }, + query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] }, }, }); expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled(); @@ -1392,7 +1392,7 @@ describe('Execution Handler', () => { }, params: {}, alertsFilter: { - query: { kql: 'kibana.alert.instance.id:1', dsl: '{}' }, + query: { kql: 'kibana.alert.instance.id:1', dsl: '{}', filters: [] }, }, }, ], @@ -1412,7 +1412,7 @@ describe('Execution Handler', () => { spaceId: 'test1', excludedAlertInstanceIds: ['foo'], alertsFilter: { - query: { kql: 'kibana.alert.instance.id:1', dsl: '{}' }, + query: { kql: 'kibana.alert.instance.id:1', dsl: '{}', filters: [] }, }, }); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledWith([ diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 21ba86dc0f25f7..420025b1cd007e 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -23,6 +23,7 @@ import { import type { PublicMethodsOf } from '@kbn/utility-types'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { type FieldMap } from '@kbn/alerts-as-data-utils'; +import { Filter } from '@kbn/es-query'; import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { RulesClient } from './rules_client'; @@ -285,11 +286,12 @@ export type UntypedRuleType = RuleType< >; export interface RawAlertsFilter extends AlertsFilter { - query: null | { + query?: { kql: string; + filters: Filter[]; dsl: string; }; - timeframe: null | AlertsFilterTimeframe; + timeframe?: AlertsFilterTimeframe; } export interface RawRuleAction extends SavedObjectAttributes { diff --git a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts index 1e55fbe047ddcb..3bf83993df1e88 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts @@ -2098,6 +2098,7 @@ describe('createGetSummarizedAlertsFn', () => { query: { kql: 'kibana.alert.rule.name:test', dsl: '{"bool":{"minimum_should_match":1,"should":[{"match":{"kibana.alert.rule.name":"test"}}]}}', + filters: [], }, timeframe: { days: [1, 2, 3, 4, 5], @@ -2147,11 +2148,12 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', days: [1, 2, 3, 4, 5], timezone: 'UTC', }, source: - "params.days.contains(doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())", + 'params.days.contains(doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())', }, }, }, @@ -2159,17 +2161,18 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', end: '17:00', start: '08:00', timezone: 'UTC', }, source: ` - def alertsDateTime = doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)); + def alertsDateTime = doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)); def alertsTime = LocalTime.of(alertsDateTime.getHour(), alertsDateTime.getMinute()); def start = LocalTime.parse(params.start); def end = LocalTime.parse(params.end); - if (end.isBefore(start)){ // overnight + if (end.isBefore(start) || end.equals(start)){ // overnight def dayEnd = LocalTime.parse("23:59:59"); def dayStart = LocalTime.parse("00:00:00"); if ((alertsTime.isAfter(start) && alertsTime.isBefore(dayEnd)) || (alertsTime.isAfter(dayStart) && alertsTime.isBefore(end))) { @@ -2231,11 +2234,12 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', days: [1, 2, 3, 4, 5], timezone: 'UTC', }, source: - "params.days.contains(doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())", + 'params.days.contains(doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())', }, }, }, @@ -2243,17 +2247,18 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', end: '17:00', start: '08:00', timezone: 'UTC', }, source: ` - def alertsDateTime = doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)); + def alertsDateTime = doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)); def alertsTime = LocalTime.of(alertsDateTime.getHour(), alertsDateTime.getMinute()); def start = LocalTime.parse(params.start); def end = LocalTime.parse(params.end); - if (end.isBefore(start)){ // overnight + if (end.isBefore(start) || end.equals(start)){ // overnight def dayEnd = LocalTime.parse("23:59:59"); def dayStart = LocalTime.parse("00:00:00"); if ((alertsTime.isAfter(start) && alertsTime.isBefore(dayEnd)) || (alertsTime.isAfter(dayStart) && alertsTime.isBefore(end))) { @@ -2315,11 +2320,12 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', days: [1, 2, 3, 4, 5], timezone: 'UTC', }, source: - "params.days.contains(doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())", + 'params.days.contains(doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())', }, }, }, @@ -2327,17 +2333,18 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', end: '17:00', start: '08:00', timezone: 'UTC', }, source: ` - def alertsDateTime = doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)); + def alertsDateTime = doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)); def alertsTime = LocalTime.of(alertsDateTime.getHour(), alertsDateTime.getMinute()); def start = LocalTime.parse(params.start); def end = LocalTime.parse(params.end); - if (end.isBefore(start)){ // overnight + if (end.isBefore(start) || end.equals(start)){ // overnight def dayEnd = LocalTime.parse("23:59:59"); def dayStart = LocalTime.parse("00:00:00"); if ((alertsTime.isAfter(start) && alertsTime.isBefore(dayEnd)) || (alertsTime.isAfter(dayStart) && alertsTime.isBefore(end))) { diff --git a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts index 4a7a727276ad44..d4fe127b240eea 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts @@ -573,10 +573,11 @@ const generateAlertsFilterDSL = (alertsFilter: AlertsFilter): QueryDslQueryConta script: { script: { source: - "params.days.contains(doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())", + 'params.days.contains(doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())', params: { days: alertsFilter.timeframe.days, timezone: alertsFilter.timeframe.timezone, + datetimeField: TIMESTAMP, }, }, }, @@ -585,12 +586,12 @@ const generateAlertsFilterDSL = (alertsFilter: AlertsFilter): QueryDslQueryConta script: { script: { source: ` - def alertsDateTime = doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)); + def alertsDateTime = doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)); def alertsTime = LocalTime.of(alertsDateTime.getHour(), alertsDateTime.getMinute()); def start = LocalTime.parse(params.start); def end = LocalTime.parse(params.end); - if (end.isBefore(start)){ // overnight + if (end.isBefore(start) || end.equals(start)){ // overnight def dayEnd = LocalTime.parse("23:59:59"); def dayStart = LocalTime.parse("00:00:00"); if ((alertsTime.isAfter(start) && alertsTime.isBefore(dayEnd)) || (alertsTime.isAfter(dayStart) && alertsTime.isBefore(end))) { @@ -610,6 +611,7 @@ const generateAlertsFilterDSL = (alertsFilter: AlertsFilter): QueryDslQueryConta start: alertsFilter.timeframe.hours.start, end: alertsFilter.timeframe.hours.end, timezone: alertsFilter.timeframe.timezone, + datetimeField: TIMESTAMP, }, }, }, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 02566a8302937b..019e20b25db1aa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -195,12 +195,18 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = (key: string, value: RuleActionAlertsFilterProperty, index: number) => { field.setValue((prevValue: RuleAction[]) => { const updatedActions = [...prevValue]; + const { alertsFilter, ...rest } = updatedActions[index]; + const updatedAlertsFilter = { ...alertsFilter }; + + if (value) { + updatedAlertsFilter[key] = value; + } else { + delete updatedAlertsFilter[key]; + } + updatedActions[index] = { - ...updatedActions[index], - alertsFilter: { - ...(updatedActions[index].alertsFilter ?? { query: null, timeframe: null }), - [key]: value, - }, + ...rest, + ...(!isEmpty(updatedAlertsFilter) ? { alertsFilter: updatedAlertsFilter } : {}), }; return updatedActions; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts index ed56bcdb3ca7d6..d8b8c85ad26a52 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/create.ts @@ -36,7 +36,7 @@ const rewriteBodyRequest: RewriteResponseCase = ({ throttle: frequency!.throttle, summary: frequency!.summary, }, - alertsFilter, + alerts_filter: alertsFilter, })), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts index 15097eb03f156a..ee2418d22262b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts @@ -26,7 +26,7 @@ const rewriteBodyRequest: RewriteResponseCase = ({ actions, ... throttle: frequency!.throttle, summary: frequency!.summary, }, - alertsFilter, + alerts_filter: alertsFilter, ...(uuid && { uuid }), })), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_query.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_query.tsx new file mode 100644 index 00000000000000..3cd22cc70a429f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_query.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import { Filter } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { EuiSwitch, EuiSpacer } from '@elastic/eui'; +import { AlertsFilter } from '@kbn/alerting-plugin/common'; +import deepEqual from 'fast-deep-equal'; +import { AlertsSearchBar } from '../alerts_search_bar'; + +interface ActionAlertsFilterQueryProps { + state?: AlertsFilter['query']; + onChange: (update?: AlertsFilter['query']) => void; +} + +export const ActionAlertsFilterQuery: React.FC = ({ + state, + onChange, +}) => { + const [query, setQuery] = useState(state ?? { kql: '', filters: [] }); + + const queryEnabled = useMemo(() => Boolean(state), [state]); + + useEffect(() => { + const nextState = queryEnabled ? query : undefined; + if (!deepEqual(state, nextState)) onChange(nextState); + }, [queryEnabled, query, state, onChange]); + + const toggleQuery = useCallback( + () => onChange(state ? undefined : query), + [state, query, onChange] + ); + const updateQuery = useCallback( + (update: Partial) => { + setQuery({ + ...query, + ...update, + }); + }, + [query, setQuery] + ); + + const onQueryChange = useCallback( + ({ query: newQuery }) => updateQuery({ kql: newQuery }), + [updateQuery] + ); + + const onFiltersUpdated = useCallback( + (filters: Filter[]) => updateQuery({ filters }), + [updateQuery] + ); + + return ( + <> + + {queryEnabled && ( + <> + + + + )} + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.test.tsx index a4c3edfda33444..ef73b3ab305c05 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.test.tsx @@ -17,7 +17,7 @@ jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({ })); describe('action_alerts_filter_timeframe', () => { - async function setup(timeframe: AlertsFilterTimeframe | null) { + async function setup(timeframe?: AlertsFilterTimeframe) { const wrapper = mountWithIntl( {}} /> ); @@ -32,7 +32,7 @@ describe('action_alerts_filter_timeframe', () => { } it('renders an unchecked switch when passed a null timeframe', async () => { - const wrapper = await setup(null); + const wrapper = await setup(); const alertsFilterTimeframeToggle = wrapper.find( '[data-test-subj="alertsFilterTimeframeToggle"]' diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.tsx index a8e276f81535f3..05fcd8fba77a74 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.tsx @@ -24,8 +24,8 @@ import { AlertsFilterTimeframe, IsoWeekday } from '@kbn/alerting-plugin/common'; import { I18N_WEEKDAY_OPTIONS_DDD, ISO_WEEKDAYS } from '../../../common/constants'; interface ActionAlertsFilterTimeframeProps { - state: AlertsFilterTimeframe | null; - onChange: (update: AlertsFilterTimeframe | null) => void; + state?: AlertsFilterTimeframe; + onChange: (update?: AlertsFilterTimeframe) => void; } const TIMEZONE_OPTIONS = moment.tz?.names().map((n) => ({ label: n })) ?? [{ label: 'UTC' }]; @@ -46,14 +46,14 @@ const useDefaultTimezone = () => { return kibanaTz; }; -const useTimeframe = (initialTimeframe: AlertsFilterTimeframe | null) => { +const useTimeframe = (initialTimeframe?: AlertsFilterTimeframe) => { const timezone = useDefaultTimezone(); const DEFAULT_TIMEFRAME = { days: [], timezone, hours: { start: '00:00', - end: '24:00', + end: '23:59', }, }; return useState(initialTimeframe || DEFAULT_TIMEFRAME); @@ -79,12 +79,12 @@ export const ActionAlertsFilterTimeframe: React.FC { - const nextState = timeframeEnabled ? timeframe : null; + const nextState = timeframeEnabled ? timeframe : undefined; if (!deepEqual(state, nextState)) onChange(nextState); }, [timeframeEnabled, timeframe, state, onChange]); const toggleTimeframe = useCallback( - () => onChange(state ? null : timeframe), + () => onChange(state ? undefined : timeframe), [state, timeframe, onChange] ); const updateTimeframe = useCallback( 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 18a8a577b7e0f8..5be6a916988334 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 @@ -351,7 +351,7 @@ describe('action_form', () => { (initialAlert.actions[index] = { ...initialAlert.actions[index], alertsFilter: { - ...(initialAlert.actions[index].alertsFilter ?? { query: null, timeframe: null }), + ...initialAlert.actions[index].alertsFilter, [key]: value, }, }) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 38d267d0148240..1743d435b722f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -61,6 +61,7 @@ import { ConnectorsSelection } from './connectors_selection'; import { ActionNotifyWhen } from './action_notify_when'; import { validateParamsForWarnings } from '../../lib/validate_params_for_warnings'; import { ActionAlertsFilterTimeframe } from './action_alerts_filter_timeframe'; +import { ActionAlertsFilterQuery } from './action_alerts_filter_query'; export type ActionTypeFormProps = { actionItem: RuleAction; @@ -405,8 +406,13 @@ export const ActionTypeForm = ({ {showActionAlertsFilter && ( <> {!hideNotifyWhen && } + setActionAlertsFilterProperty('query', query, index)} + /> + setActionAlertsFilterProperty('timeframe', timeframe, index)} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/alerts_search_bar.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/alerts_search_bar.tsx index 6a170363d34a89..49b1f9905cd7f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/alerts_search_bar.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/alerts_search_bar.tsx @@ -19,9 +19,16 @@ export function AlertsSearchBar({ appName, featureIds, query, + filters, onQueryChange, + onFiltersUpdated, rangeFrom, rangeTo, + showFilterBar = false, + showDatePicker = true, + showSubmitButton = true, + placeholder = SEARCH_BAR_PLACEHOLDER, + submitOnBlur = false, }: AlertsSearchBarProps) { const { unifiedSearch: { @@ -52,14 +59,20 @@ export function AlertsSearchBar({ ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts index 2c94c250c168af..b7616333c199a7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Filter } from '@kbn/es-query'; import { ValidFeatureId } from '@kbn/rule-data-utils'; export type QueryLanguageType = 'lucene' | 'kuery'; @@ -15,8 +16,15 @@ export interface AlertsSearchBarProps { rangeFrom?: string; rangeTo?: string; query?: string; + filters?: Filter[]; + showFilterBar?: boolean; + showDatePicker?: boolean; + showSubmitButton?: boolean; + placeholder?: string; + submitOnBlur?: boolean; onQueryChange: (query: { dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' }; query?: string; }) => void; + onFiltersUpdated?: (filters: Filter[]) => void; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts index 8ac7c382762339..54f3871928fb35 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts @@ -13,6 +13,7 @@ import { IntervalSchedule, RuleActionAlertsFilterProperty, } from '@kbn/alerting-plugin/common'; +import { isEmpty } from 'lodash/fp'; import { Rule, RuleAction } from '../../../types'; import { DEFAULT_FREQUENCY } from '../../../common/constants'; @@ -237,12 +238,18 @@ export const ruleReducer = ( return state; } else { const oldAction = rule.actions.splice(index, 1)[0]; + const { alertsFilter, ...rest } = oldAction; + const updatedAlertsFilter = { ...alertsFilter }; + + if (value) { + updatedAlertsFilter[key] = value; + } else { + delete updatedAlertsFilter[key]; + } + const updatedAction = { - ...oldAction, - alertsFilter: { - ...(oldAction.alertsFilter ?? { timeframe: null, query: null }), - [key]: value, - }, + ...rest, + ...(!isEmpty(updatedAlertsFilter) ? { alertsFilter: updatedAlertsFilter } : {}), }; rule.actions.splice(index, 0, updatedAction); return { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index 7b5f41f4448d2d..410fee01c71f51 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -1244,8 +1244,7 @@ instanceStateValue: true throttle: null, summary: true, alertsFilter: { - timeframe: null, - query: { kql: 'kibana.alert.rule.name:abc' }, + query: { kql: 'kibana.alert.rule.name:abc', filters: [] }, }, }); @@ -1307,8 +1306,7 @@ instanceStateValue: true throttle: null, summary: true, alertsFilter: { - timeframe: null, - query: { kql: 'kibana.alert.instance.id:1' }, + query: { kql: 'kibana.alert.instance.id:1', filters: [] }, }, }); @@ -1383,7 +1381,6 @@ instanceStateValue: true timezone: 'UTC', hours: { start, end }, }, - query: null, }, }); From 085686fceb79cc86face1179582341d7b98ac811 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 21 Apr 2023 19:26:18 -0400 Subject: [PATCH 17/65] [Dashboard] Removes Reload on Clone / Replace Panel (#155561) Removes the page reload on clone and replace panel to make them feel snappier. --- .../public/dashboard_actions/replace_panel_flyout.tsx | 1 - .../dashboard_container/embeddable/api/panel_management.ts | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx index d5d5c439a47450..4a2ac8f41d6a65 100644 --- a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx @@ -81,7 +81,6 @@ export class ReplacePanelFlyout extends React.Component { } as DashboardPanelState, }, }); - container.reload(); this.showToast(name); this.props.onClose(); diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts index ee57970a93cd4f..cb2ce9af37bcd8 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts @@ -86,11 +86,7 @@ export async function replacePanel( }; } - await this.updateInput({ - panels, - lastReloadRequestTime: new Date().getTime(), - }); - + await this.updateInput({ panels }); return panelId; } From d4f2639377f190340d10b85eb449af51196fce04 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 21 Apr 2023 18:14:57 -0600 Subject: [PATCH 18/65] [Maps] fix maps listing page 'Cannot update a component while rendering a different component' (#155545) To view problem 1) Install sample data set 2) Open maps listing page. Notice the following console warning Screen Shot 2023-04-21 at 9 19 16 AM PR removes side effect during `MapsListViewComp` render that caused [ScreenReaderRouteAnnouncements](https://github.com/elastic/kibana/blob/0cd7973e1ff4693094e802b2cd3b6d711007bc5c/packages/core/chrome/core-chrome-browser-internal/src/ui/header/screen_reader_a11y.tsx) to update state. PR also refactors LoadListAndRender into a functional component with hooks. That change is not needed but was done to isolate the issue. Figured its worth keeping. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../routes/list_page/load_list_and_render.tsx | 80 ++++++++----------- .../routes/list_page/maps_list_view.tsx | 12 ++- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 5 files changed, 43 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx index 26cb872006deeb..f988c5b11ef98b 100644 --- a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import React, { Component } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { useState, useEffect } from 'react'; import { Redirect } from 'react-router-dom'; import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { ScopedHistory } from '@kbn/core/public'; -import { getToasts } from '../../kibana_services'; import { MapsListView } from './maps_list_view'; import { APP_ID } from '../../../common/constants'; import { mapsClient } from '../../content_management'; @@ -20,49 +18,39 @@ interface Props { stateTransfer: EmbeddableStateTransfer; } -export class LoadListAndRender extends Component { - _isMounted: boolean = false; - state = { - mapsLoaded: false, - hasSavedMaps: null, - }; - - componentDidMount() { - this._isMounted = true; - this.props.stateTransfer.clearEditorState(APP_ID); - this._loadMapsList(); - } - - componentWillUnmount() { - this._isMounted = false; +export function LoadListAndRender(props: Props) { + const [mapsLoaded, setMapsLoaded] = useState(false); + const [hasSavedMaps, setHasSavedMaps] = useState(true); + + useEffect(() => { + props.stateTransfer.clearEditorState(APP_ID); + + let ignore = false; + mapsClient + .search({ limit: 1 }) + .then((results) => { + if (!ignore) { + setHasSavedMaps(results.hits.length > 0); + setMapsLoaded(true); + } + }) + .catch((err) => { + if (!ignore) { + setMapsLoaded(true); + setHasSavedMaps(false); + } + }); + return () => { + ignore = true; + }; + // only run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!mapsLoaded) { + // do not render loading state to avoid UI flash when listing page is displayed + return null; } - async _loadMapsList() { - try { - const results = await mapsClient.search({ limit: 1 }); - if (this._isMounted) { - this.setState({ mapsLoaded: true, hasSavedMaps: !!results.hits.length }); - } - } catch (err) { - if (this._isMounted) { - this.setState({ mapsLoaded: true, hasSavedMaps: false }); - getToasts().addDanger({ - title: i18n.translate('xpack.maps.mapListing.errorAttemptingToLoadSavedMaps', { - defaultMessage: `Unable to load maps`, - }), - text: `${err}`, - }); - } - } - } - - render() { - const { mapsLoaded, hasSavedMaps } = this.state; - - if (mapsLoaded) { - return hasSavedMaps ? : ; - } else { - return null; - } - } + return hasSavedMaps ? : ; } diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index 95063f728a8fc5..d98444057097dd 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, memo } from 'react'; +import React, { useCallback, memo, useEffect } from 'react'; import type { SavedObjectsFindOptionsReference, ScopedHistory } from '@kbn/core/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; @@ -72,8 +72,14 @@ function MapsListViewComp({ history }: Props) { const listingLimit = getUiSettings().get(SAVED_OBJECTS_LIMIT_SETTING); const initialPageSize = getUiSettings().get(SAVED_OBJECTS_PER_PAGE_SETTING); - getCoreChrome().docTitle.change(APP_NAME); - getCoreChrome().setBreadcrumbs([{ text: APP_NAME }]); + // TLDR; render should be side effect free + // + // setBreadcrumbs fires observables which cause state changes in ScreenReaderRouteAnnouncements. + // wrap chrome updates in useEffect to avoid potentially causing state changes in other component during render phase. + useEffect(() => { + getCoreChrome().docTitle.change(APP_NAME); + getCoreChrome().setBreadcrumbs([{ text: APP_NAME }]); + }, []); const findMaps = useCallback( async ( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index dbc92e04c393f7..58305363bb55e0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -20560,7 +20560,6 @@ "xpack.maps.mapActions.removeFeatureError": "Impossible de retirer la fonctionnalité de l’index.", "xpack.maps.mapListing.entityName": "carte", "xpack.maps.mapListing.entityNamePlural": "cartes", - "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "Impossible de charger les cartes", "xpack.maps.mapSavedObjectLabel": "Carte", "xpack.maps.mapSettingsPanel.addCustomIcon": "Ajouter une icône personnalisée", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "Ajuster automatiquement la carte aux limites de données", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 22cfeb42e69d82..a17220cadd2d2a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20560,7 +20560,6 @@ "xpack.maps.mapActions.removeFeatureError": "インデックスから機能を削除できません。", "xpack.maps.mapListing.entityName": "マップ", "xpack.maps.mapListing.entityNamePlural": "マップ", - "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "マップを読み込めません", "xpack.maps.mapSavedObjectLabel": "マップ", "xpack.maps.mapSettingsPanel.addCustomIcon": "カスタムアイコンを追加", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "自動的にマップをデータ境界に合わせる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 67bf31893d10d6..057233f627acec 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20560,7 +20560,6 @@ "xpack.maps.mapActions.removeFeatureError": "无法从索引中移除特征。", "xpack.maps.mapListing.entityName": "地图", "xpack.maps.mapListing.entityNamePlural": "地图", - "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "无法加载地图", "xpack.maps.mapSavedObjectLabel": "地图", "xpack.maps.mapSettingsPanel.addCustomIcon": "添加定制图标", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "使地图自动适应数据边界", From ef64acf405ed7577426571fbb87a898c1934bc81 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 21 Apr 2023 18:20:26 -0600 Subject: [PATCH 19/65] [RAM] Adds revision to event-log (#153716) ## Summary Follow on from https://github.com/elastic/kibana/pull/151388 & https://github.com/elastic/kibana/pull/147398, which includes the rule's current `revision` when writing to the kibana event-log. Note: Added as `kibana.alert.rule.revision` instead of as ECS field `rule.version` as the [ECS docs](https://www.elastic.co/guide/en/ecs/current/ecs-rule.html#field-rule-version) conflate `version` & `revision` and figured it was best to be explicit. If we do indeed want to use `rule.version` I'll make the change.

### Checklist Delete any items that are not applicable to this PR. - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../plugins/alerting/common/alert_summary.ts | 1 + .../lib/alert_summary_from_event_log.test.ts | 2 + .../lib/alert_summary_from_event_log.ts | 1 + .../alerting_event_logger.test.ts | 1 + .../alerting_event_logger.ts | 5 + ...eate_alert_event_log_record_object.test.ts | 6 ++ .../create_alert_event_log_record_object.ts | 3 + .../routes/get_rule_alert_summary.test.ts | 1 + .../legacy/get_alert_instance_summary.test.ts | 1 + .../rules_client/lib/recover_rule_alerts.ts | 1 + .../server/rules_client/tests/disable.test.ts | 9 ++ .../tests/get_alert_summary.test.ts | 1 + .../alerting/server/task_runner/fixtures.ts | 1 + .../transform_action_params.test.ts | 102 +++++++++--------- .../plugins/event_log/generated/mappings.json | 3 + x-pack/plugins/event_log/generated/schemas.ts | 1 + x-pack/plugins/event_log/scripts/mappings.js | 3 + .../rule_execution_log/__mocks__/index.ts | 1 + .../client_for_executors/client.ts | 5 +- .../client_for_executors/client_interface.ts | 5 + .../event_log/event_log_writer.ts | 4 + .../rule_schema/model/rule_schemas.mock.ts | 2 +- .../create_security_rule_type_wrapper.ts | 1 + .../rule_types/es_query/rule_type.test.ts | 2 +- .../index_threshold/rule_type.test.ts | 8 +- .../lib/rule_api/rule_summary.test.ts | 2 + .../rule_details/components/rule.test.tsx | 1 + .../components/rule_route.test.tsx | 1 + .../rule_details/components/test_helpers.ts | 1 + .../tests/alerting/get_alert_summary.ts | 1 + .../alerting/group1/get_alert_summary.ts | 2 + .../event_log/service_api_integration.ts | 1 + 32 files changed, 121 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/alerting/common/alert_summary.ts b/x-pack/plugins/alerting/common/alert_summary.ts index a465d6af2daede..ed6cf325e20b1b 100644 --- a/x-pack/plugins/alerting/common/alert_summary.ts +++ b/x-pack/plugins/alerting/common/alert_summary.ts @@ -29,6 +29,7 @@ export interface AlertSummary { errorMessages: Array<{ date: string; message: string }>; alerts: Record; executionDuration: ExecutionDuration; + revision: number; } export interface AlertStatus { diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts index dbcb75a7816e77..45f8d84fd4a98a 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts @@ -42,6 +42,7 @@ describe('alertSummaryFromEventLog', () => { "lastRun": undefined, "muteAll": false, "name": "rule-name", + "revision": 0, "ruleTypeId": "123", "status": "OK", "statusEndDate": "2020-06-18T01:00:00.000Z", @@ -88,6 +89,7 @@ describe('alertSummaryFromEventLog', () => { "lastRun": undefined, "muteAll": true, "name": "rule-name-2", + "revision": 0, "ruleTypeId": "456", "status": "OK", "statusEndDate": "2020-06-18T03:00:00.000Z", diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts index 9dc4a4fec5f8ea..d316b1b5bf01ce 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts @@ -40,6 +40,7 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams) average: 0, valuesWithTimestamp: {}, }, + revision: rule.revision, }; const alerts = new Map(); diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 62c8d9eed29fdf..2b6075cd49f499 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -55,6 +55,7 @@ const context: RuleContextOpts = { spaceId: 'test-space', executionId: 'abcd-efgh-ijklmnop', taskScheduledAt: new Date('2020-01-01T00:00:00.000Z'), + ruleRevision: 0, }; const contextWithScheduleDelay = { ...context, taskScheduleDelay: 7200000 }; diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index 60d9d21f161131..3af760488a2593 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -30,6 +30,7 @@ export interface RuleContextOpts { executionId: string; taskScheduledAt: Date; ruleName?: string; + ruleRevision?: number; } type RuleContext = RuleContextOpts & { @@ -266,6 +267,7 @@ export function createAlertRecord(context: RuleContextOpts, alert: AlertOpts) { ruleName: context.ruleName, flapping: alert.flapping, maintenanceWindowIds: alert.maintenanceWindowIds, + ruleRevision: context.ruleRevision, }); } @@ -296,6 +298,7 @@ export function createActionExecuteRecord(context: RuleContextOpts, action: Acti ], ruleName: context.ruleName, alertSummary: action.alertSummary, + ruleRevision: context.ruleRevision, }); } @@ -322,6 +325,7 @@ export function createExecuteTimeoutRecord(context: RuleContextOpts) { }, ], ruleName: context.ruleName, + ruleRevision: context.ruleRevision, }); } @@ -334,6 +338,7 @@ export function initializeExecuteRecord(context: RuleContext) { spaceId: context.spaceId, executionId: context.executionId, action: EVENT_LOG_ACTIONS.execute, + ruleRevision: context.ruleRevision, task: { scheduled: context.taskScheduledAt.toISOString(), scheduleDelay: Millis2Nanos * context.taskScheduleDelay, diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts index e62ec213cba0f1..6b18d1aac93ddf 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts @@ -36,6 +36,7 @@ describe('createAlertEventLogRecordObject', () => { ruleType, consumer: 'rule-consumer', action: 'execute-start', + ruleRevision: 0, timestamp: '1970-01-01T00:00:00.000Z', task: { scheduled: '1970-01-01T00:00:00.000Z', @@ -66,6 +67,7 @@ describe('createAlertEventLogRecordObject', () => { execution: { uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', }, + revision: 0, rule_type_id: 'test', }, maintenance_window_ids: MAINTENANCE_WINDOW_IDS, @@ -107,6 +109,7 @@ describe('createAlertEventLogRecordObject', () => { group: 'group 1', message: 'message text here', namespace: 'default', + ruleRevision: 0, state: { start: '1970-01-01T00:00:00.000Z', end: '1970-01-01T00:05:00.000Z', @@ -139,6 +142,7 @@ describe('createAlertEventLogRecordObject', () => { execution: { uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', }, + revision: 0, rule_type_id: 'test', }, maintenance_window_ids: MAINTENANCE_WINDOW_IDS, @@ -182,6 +186,7 @@ describe('createAlertEventLogRecordObject', () => { group: 'group 1', message: 'action execution start', namespace: 'default', + ruleRevision: 0, state: { start: '1970-01-01T00:00:00.000Z', end: '1970-01-01T00:05:00.000Z', @@ -224,6 +229,7 @@ describe('createAlertEventLogRecordObject', () => { execution: { uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', }, + revision: 0, rule_type_id: 'test', }, maintenance_window_ids: MAINTENANCE_WINDOW_IDS, diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts index 97284f0ce9f78e..251df68e5267f7 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts @@ -43,6 +43,7 @@ interface CreateAlertEventLogRecordParams { recovered: number; }; maintenanceWindowIds?: string[]; + ruleRevision?: number; } export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecordParams): Event { @@ -62,6 +63,7 @@ export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecor alertUuid, alertSummary, maintenanceWindowIds, + ruleRevision, } = params; const alerting = params.instanceId || group || alertSummary @@ -97,6 +99,7 @@ export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecor ...(maintenanceWindowIds ? { maintenance_window_ids: maintenanceWindowIds } : {}), ...(alertUuid ? { uuid: alertUuid } : {}), rule: { + revision: ruleRevision, rule_type_id: ruleType.id, ...(consumer ? { consumer } : {}), ...(executionId diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts index a6b0e88bbf5af6..f7fe1a3406e9cb 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts @@ -38,6 +38,7 @@ describe('getRuleAlertSummaryRoute', () => { status: 'OK', errorMessages: [], alerts: {}, + revision: 0, executionDuration: { average: 1, valuesWithTimestamp: { diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts index 672ccee369aac9..a78fcd7f86f650 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts @@ -47,6 +47,7 @@ describe('getAlertInstanceSummaryRoute', () => { average: 0, valuesWithTimestamp: {}, }, + revision: 0, }; it('gets alert instance summary', async () => { diff --git a/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts b/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts index bf8d10a341597b..9648f23dc05d2b 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts @@ -43,6 +43,7 @@ export const recoverRuleAlerts = async ( const event = createAlertEventLogRecordObject({ ruleId: id, ruleName: attributes.name, + ruleRevision: attributes.revision, ruleType: context.ruleTypeRegistry.get(attributes.alertTypeId), consumer: attributes.consumer, instanceId: alertId, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index c09b491dbbdcca..b135f9b0ee23d8 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -100,6 +100,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: true, + revision: 0, scheduledTaskId: '1', actions: [ { @@ -224,6 +225,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, + revision: 0, scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -277,6 +279,7 @@ describe('disable()', () => { }, params: { alertId: '1', + revision: 0, }, ownerId: null, }); @@ -296,6 +299,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, + revision: 0, scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -333,6 +337,7 @@ describe('disable()', () => { uuid: 'uuid-1', rule: { consumer: 'myApp', + revision: 0, rule_type_id: '123', }, }, @@ -379,6 +384,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, + revision: 0, scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -425,6 +431,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + revision: 0, scheduledTaskId: '1', updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', @@ -517,6 +524,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + revision: 0, scheduledTaskId: null, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', @@ -565,6 +573,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + revision: 0, scheduledTaskId: null, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts index 212977a8381c2f..dbfcc5f3fc0171 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts @@ -189,6 +189,7 @@ describe('getAlertSummary()', () => { "lastRun": "2019-02-12T21:01:32.479Z", "muteAll": false, "name": "rule-name", + "revision": 0, "ruleTypeId": "123", "status": "Active", "statusEndDate": "2019-02-12T21:01:22.479Z", diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index 0b5cd235185d05..9be9064908fcb5 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -409,6 +409,7 @@ export const mockAAD = { execution: { uuid: 'c35db7cc-5bf7-46ea-b43f-b251613a5b72' }, name: 'test-rule', producer: 'infrastructure', + revision: 0, rule_type_id: 'metrics.alert.threshold', uuid: '0de91960-7643-11ed-b719-bb9db8582cb6', tags: [], diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts index cd581afafa2665..a8889bc620c83f 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts @@ -182,10 +182,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"1\\" exists", - } - `); + Object { + "message": "Value \\"1\\" exists", + } + `); }); test('alertName is passed to templates', () => { @@ -212,10 +212,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"alert-name\\" exists", - } - `); + Object { + "message": "Value \\"alert-name\\" exists", + } + `); }); test('tags is passed to templates', () => { @@ -242,10 +242,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"tag-A,tag-B\\" exists", - } - `); + Object { + "message": "Value \\"tag-A,tag-B\\" exists", + } + `); }); test('undefined tags is passed to templates', () => { @@ -271,10 +271,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"\\" is undefined and renders as empty string", - } - `); + Object { + "message": "Value \\"\\" is undefined and renders as empty string", + } + `); }); test('empty tags is passed to templates', () => { @@ -301,10 +301,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"\\" is an empty array and renders as empty string", - } - `); + Object { + "message": "Value \\"\\" is an empty array and renders as empty string", + } + `); }); test('spaceId is passed to templates', () => { @@ -331,10 +331,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"spaceId-A\\" exists", - } - `); + Object { + "message": "Value \\"spaceId-A\\" exists", + } + `); }); test('alertInstanceId is passed to templates', () => { @@ -361,10 +361,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"2\\" exists", - } - `); + Object { + "message": "Value \\"2\\" exists", + } + `); }); test('alertActionGroup is passed to templates', () => { @@ -391,10 +391,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"action-group\\" exists", - } - `); + Object { + "message": "Value \\"action-group\\" exists", + } + `); }); test('alertActionGroupName is passed to templates', () => { @@ -421,10 +421,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"Action Group\\" exists", - } - `); + Object { + "message": "Value \\"Action Group\\" exists", + } + `); }); test('rule variables are passed to templates', () => { @@ -451,10 +451,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"1\\", \\"alert-name\\", \\"spaceId-A\\" and \\"tag-A,tag-B\\" exist", - } - `); + Object { + "message": "Value \\"1\\", \\"alert-name\\", \\"spaceId-A\\" and \\"tag-A,tag-B\\" exist", + } + `); }); test('rule alert variables are passed to templates', () => { @@ -482,10 +482,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"2\\", \\"action-group\\", \\"uuid-1\\" and \\"Action Group\\" exist", - } - `); + Object { + "message": "Value \\"2\\", \\"action-group\\", \\"uuid-1\\" and \\"Action Group\\" exist", + } + `); }); test('date is passed to templates', () => { @@ -613,10 +613,10 @@ describe('transformActionParams', () => { flapping: true, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"true\\" exists", - } - `); + Object { + "message": "Value \\"true\\" exists", + } + `); }); }); @@ -665,9 +665,9 @@ describe('transformSummaryActionParams', () => { const result = transformSummaryActionParams({ ...params, actionParams }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"{\\"@timestamp\\":\\"2022-12-07T15:38:43.472Z\\",\\"event\\":{\\"kind\\":\\"signal\\",\\"action\\":\\"active\\"},\\"kibana\\":{\\"version\\":\\"8.7.0\\",\\"space_ids\\":[\\"default\\"],\\"alert\\":{\\"instance\\":{\\"id\\":\\"*\\"},\\"uuid\\":\\"2d3e8fe5-3e8b-4361-916e-9eaab0bf2084\\",\\"status\\":\\"active\\",\\"workflow_status\\":\\"open\\",\\"reason\\":\\"system.cpu is 90% in the last 1 min for all hosts. Alert when > 50%.\\",\\"time_range\\":{\\"gte\\":\\"2022-01-01T12:00:00.000Z\\"},\\"start\\":\\"2022-12-07T15:23:13.488Z\\",\\"duration\\":{\\"us\\":100000},\\"flapping\\":false,\\"rule\\":{\\"category\\":\\"Metric threshold\\",\\"consumer\\":\\"alerts\\",\\"execution\\":{\\"uuid\\":\\"c35db7cc-5bf7-46ea-b43f-b251613a5b72\\"},\\"name\\":\\"test-rule\\",\\"producer\\":\\"infrastructure\\",\\"rule_type_id\\":\\"metrics.alert.threshold\\",\\"uuid\\":\\"0de91960-7643-11ed-b719-bb9db8582cb6\\",\\"tags\\":[]}}}}\\", \\"http://ruleurl\\" and \\"{\\"foo\\":\\"bar\\",\\"foo_bar\\":true,\\"name\\":\\"test-rule\\",\\"id\\":\\"1\\"}\\" exist", - } + Object { + "message": "Value \\"{\\"@timestamp\\":\\"2022-12-07T15:38:43.472Z\\",\\"event\\":{\\"kind\\":\\"signal\\",\\"action\\":\\"active\\"},\\"kibana\\":{\\"version\\":\\"8.7.0\\",\\"space_ids\\":[\\"default\\"],\\"alert\\":{\\"instance\\":{\\"id\\":\\"*\\"},\\"uuid\\":\\"2d3e8fe5-3e8b-4361-916e-9eaab0bf2084\\",\\"status\\":\\"active\\",\\"workflow_status\\":\\"open\\",\\"reason\\":\\"system.cpu is 90% in the last 1 min for all hosts. Alert when > 50%.\\",\\"time_range\\":{\\"gte\\":\\"2022-01-01T12:00:00.000Z\\"},\\"start\\":\\"2022-12-07T15:23:13.488Z\\",\\"duration\\":{\\"us\\":100000},\\"flapping\\":false,\\"rule\\":{\\"category\\":\\"Metric threshold\\",\\"consumer\\":\\"alerts\\",\\"execution\\":{\\"uuid\\":\\"c35db7cc-5bf7-46ea-b43f-b251613a5b72\\"},\\"name\\":\\"test-rule\\",\\"producer\\":\\"infrastructure\\",\\"revision\\":0,\\"rule_type_id\\":\\"metrics.alert.threshold\\",\\"uuid\\":\\"0de91960-7643-11ed-b719-bb9db8582cb6\\",\\"tags\\":[]}}}}\\", \\"http://ruleurl\\" and \\"{\\"foo\\":\\"bar\\",\\"foo_bar\\":true,\\"name\\":\\"test-rule\\",\\"id\\":\\"1\\"}\\" exist", + } `); }); diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index f50a1c0ef3321a..e802567b2ab7f5 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -398,6 +398,9 @@ } } }, + "revision": { + "type": "long" + }, "rule_type_id": { "type": "keyword", "ignore_above": 1024 diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 6706db96353978..8621df23020bd7 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -178,6 +178,7 @@ export const EventSchema = schema.maybe( ), }) ), + revision: ecsStringOrNumber(), rule_type_id: ecsString(), }) ), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 7081c321cc6595..768988aa3c07bb 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -177,6 +177,9 @@ exports.EcsCustomPropertyMappings = { }, }, }, + revision: { + type: 'long', + }, rule_type_id: { type: 'keyword', ignore_above: 1024, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts index c0cb0cefa234ef..c68eab1baec36b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts @@ -40,6 +40,7 @@ const ruleExecutionLogForExecutorsMock = { ruleId: context.ruleId ?? 'some rule id', ruleUuid: context.ruleUuid ?? 'some rule uuid', ruleName: context.ruleName ?? 'Some rule', + ruleRevision: context.ruleRevision ?? 0, ruleType: context.ruleType ?? 'some rule type', spaceId: context.spaceId ?? 'some space id', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts index a0cbe754d6b3c2..a8d61aa7dda842 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts @@ -50,7 +50,7 @@ export const createClientForExecutors = ( const baseLogSuffix = baseCorrelationIds.getLogSuffix(); const baseLogMeta = baseCorrelationIds.getLogMeta(); - const { executionId, ruleId, ruleUuid, ruleName, ruleType, spaceId } = context; + const { executionId, ruleId, ruleUuid, ruleName, ruleRevision, ruleType, spaceId } = context; const client: IRuleExecutionLogForExecutors = { get context() { @@ -140,6 +140,7 @@ export const createClientForExecutors = ( ruleId, ruleUuid, ruleName, + ruleRevision, ruleType, spaceId, executionId, @@ -202,6 +203,7 @@ export const createClientForExecutors = ( ruleId, ruleUuid, ruleName, + ruleRevision, ruleType, spaceId, executionId, @@ -213,6 +215,7 @@ export const createClientForExecutors = ( ruleId, ruleUuid, ruleName, + ruleRevision, ruleType, spaceId, executionId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts index cb65b42d50b69d..54cad1f72be075 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts @@ -92,6 +92,11 @@ export interface RuleExecutionContext { */ ruleName: string; + /** + * Current revision of the rule being execution (rule.revision) + */ + ruleRevision: number; + /** * Alerting Framework's rule type id of the rule being executed. */ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts index aa1fcf36aba68d..ceed2f1d3a739a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts @@ -31,6 +31,7 @@ export interface BaseArgs { ruleId: string; ruleUuid: string; ruleName: string; + ruleRevision: number; ruleType: string; spaceId: string; executionId: string; @@ -83,6 +84,7 @@ export const createEventLogWriter = (eventLogService: IEventLogService): IEventL execution: { uuid: args.executionId, }, + revision: args.ruleRevision, }, }, space_ids: [args.spaceId], @@ -126,6 +128,7 @@ export const createEventLogWriter = (eventLogService: IEventLogService): IEventL status: args.newStatus, status_order: ruleExecutionStatusToNumber(args.newStatus), }, + revision: args.ruleRevision, }, }, space_ids: [args.spaceId], @@ -167,6 +170,7 @@ export const createEventLogWriter = (eventLogService: IEventLogService): IEventL uuid: args.executionId, metrics: args.metrics, }, + revision: args.ruleRevision, }, }, space_ids: [args.spaceId], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts index 10a213a5d8b2fe..44fa44ee01892d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts @@ -213,10 +213,10 @@ export const getRuleConfigMock = (type: string = 'rule-type'): SanitizedRuleConf consumer: 'sample consumer', notifyWhen: null, producer: 'sample producer', + revision: 0, ruleTypeId: `${type}-id`, ruleTypeName: type, muteAll: false, - revision: 0, snoozeSchedule: [], }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index a22a95adf307b9..e2650f0a142f21 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -124,6 +124,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ruleId: rule.id, ruleUuid: params.ruleId, ruleName: rule.name, + ruleRevision: rule.revision, ruleType: rule.ruleTypeId, spaceId, }, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts index 8b1679241d9c9e..fded7e8ee37b9e 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts @@ -713,10 +713,10 @@ async function invokeExecutor({ tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts index 92664d05ff9abc..b424fb742b5ab5 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts @@ -203,10 +203,10 @@ describe('ruleType', () => { tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, @@ -270,10 +270,10 @@ describe('ruleType', () => { tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, @@ -337,10 +337,10 @@ describe('ruleType', () => { tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, @@ -403,10 +403,10 @@ describe('ruleType', () => { tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts index 43beb66b40f9ee..889f2634eacc92 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts @@ -28,6 +28,7 @@ describe('loadRuleSummary', () => { lastRun: '2021-04-01T22:18:27.609Z', muteAll: false, name: 'test', + revision: 0, ruleTypeId: '.index-threshold', status: 'OK', statusEndDate: '2021-04-01T22:19:25.174Z', @@ -55,6 +56,7 @@ describe('loadRuleSummary', () => { last_run: '2021-04-01T22:18:27.609Z', mute_all: false, name: 'test', + revision: 0, rule_type_id: '.index-threshold', status: 'OK', status_end_date: '2021-04-01T22:19:25.174Z', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx index f3775e5de50664..821d4edf16c66a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx @@ -481,6 +481,7 @@ function mockRuleSummary(overloads: Partial = {}): RuleSummary { throttle: '', enabled: true, errorMessages: [], + revision: 0, statusStartDate: fake2MinutesAgo.toISOString(), statusEndDate: fakeNow.toISOString(), alerts: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx index 117bca134f0f9b..1826894ef70f9c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx @@ -162,6 +162,7 @@ function mockRuleSummary(overloads: Partial = {}): any { throttle: null, enabled: true, errorMessages: [], + revision: 0, statusStartDate: fake2MinutesAgo.toISOString(), statusEndDate: fakeNow.toISOString(), alerts: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/test_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/test_helpers.ts index 4146ac80063253..d72e381c170ab6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/test_helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/test_helpers.ts @@ -95,6 +95,7 @@ export function mockRuleSummary(overloads: Partial = {}): RuleSumma throttle: '', enabled: true, errorMessages: [], + revision: 0, statusStartDate: '2022-03-21T07:40:46-07:00', statusEndDate: '2022-03-25T07:40:46-07:00', alerts: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts index f0ba9dc4519375..7ad1a54de24856 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts @@ -78,6 +78,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo expect(stableBody).to.eql({ name: 'abc', tags: ['foo'], + revision: 0, rule_type_id: 'test.noop', consumer: 'alertsFixture', status: 'OK', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts index 6f8de29437621c..9be9db54904aab 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts @@ -68,6 +68,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo id: createdRule.id, name: 'abc', tags: ['foo'], + revision: 0, rule_type_id: 'test.noop', consumer: 'alertsFixture', status: 'OK', @@ -106,6 +107,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo id: createdRule.id, name: 'abc', tags: ['foo'], + revision: 0, rule_type_id: 'test.noop', consumer: 'alertsFixture', status: 'OK', diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index 2bd65ef5dcd3e9..dc244a51bb1839 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -163,6 +163,7 @@ export default function ({ getService }: FtrProviderContext) { execution_gap_duration_s: 3000, }, }, + revision: 0, }, }, alerting: { From a108b8c9c6c641a68aaa3e343f6a7e823f1340a8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 21 Apr 2023 18:46:57 -0600 Subject: [PATCH 20/65] [maps] metrics mask (#154983) Fixes https://github.com/elastic/kibana/issues/55520 PR adds ability to set mask configuration for metric fields to hide features client side that are outside the range of the mask. Screen Shot 2023-04-19 at 3 45 15 PM Screen Shot 2023-04-19 at 3 44 45 PM For vector layer, I could not use easier solution of filter expression since filter expressions do not support feature-state. Instead, implementation sets opacity paint property to 0. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/common/constants.ts | 9 +- .../source_descriptor_types.ts | 5 + .../classes/fields/agg/agg_field_types.ts | 3 + .../classes/fields/agg/count_agg_field.ts | 10 +- .../classes/fields/agg/es_agg_factory.ts | 3 + .../fields/agg/top_term_percentage_field.ts | 4 + .../layers/heatmap_layer/heatmap_layer.ts | 16 +- .../blended_vector_layer.ts | 6 +- .../classes/layers/vector_layer/mask.ts | 193 ++++++++++++++++++ .../mvt_vector_layer.test.tsx | 7 + .../layers/vector_layer/vector_layer.tsx | 76 ++++++- .../sources/es_agg_source/es_agg_source.ts | 14 +- .../es_agg_source/get_agg_display_name.ts | 48 +++++ .../classes/sources/es_agg_source/index.ts | 1 + .../classes/sources/es_agg_source/types.ts | 6 + .../update_source_editor.test.tsx.snap | 4 + .../es_geo_grid_source/es_geo_grid_source.tsx | 19 ++ .../update_source_editor.test.tsx | 1 + .../update_source_editor.tsx | 4 +- .../es_geo_line_source/es_geo_line_source.tsx | 7 + .../update_source_editor.tsx | 8 +- .../es_pew_pew_source/es_pew_pew_source.tsx | 8 +- ...rce_editor.js => update_source_editor.tsx} | 35 ++-- .../es_term_source/es_term_source.test.js | 4 +- .../sources/es_term_source/es_term_source.ts | 5 +- .../components/legend/heatmap_legend.tsx | 31 ++- .../vector/components/legend/mask_legend.tsx | 95 +++++++++ .../components/legend/vector_style_legend.tsx | 67 +++++- .../properties/dynamic_color_property.tsx | 10 +- .../properties/static_color_property.ts | 10 +- .../classes/styles/vector/vector_style.tsx | 17 +- .../metrics_editor.test.tsx.snap | 2 + .../metrics_editor/mask_expression/index.ts | 8 + .../mask_expression/mask_editor.tsx | 183 +++++++++++++++++ .../mask_expression/mask_expression.tsx | 104 ++++++++++ .../metrics_editor/metric_editor.tsx | 15 ++ .../metrics_editor/metric_select.tsx | 33 +-- .../metrics_editor/metrics_editor.test.tsx | 1 + .../metrics_editor/metrics_editor.tsx | 4 + .../metrics_expression.test.tsx.snap | 2 + .../resources/metrics_expression.tsx | 1 + .../attribution_form_row.test.tsx.snap | 6 +- .../layer_settings/attribution_form_row.tsx | 11 +- .../layer_settings/attribution_popover.tsx | 3 +- .../style_settings/style_settings.tsx | 2 +- .../tooltip_control/tooltip_control.test.tsx | 3 + .../tooltip_control/tooltip_control.tsx | 9 + .../connected_components/panel_strings.ts | 9 + .../translations/translations/fr-FR.json | 12 -- .../translations/translations/ja-JP.json | 14 +- .../translations/translations/zh-CN.json | 14 +- 51 files changed, 1022 insertions(+), 140 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts create mode 100644 x-pack/plugins/maps/public/classes/sources/es_agg_source/get_agg_display_name.ts rename x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/{update_source_editor.js => update_source_editor.tsx} (74%) create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/components/legend/mask_legend.tsx create mode 100644 x-pack/plugins/maps/public/components/metrics_editor/mask_expression/index.ts create mode 100644 x-pack/plugins/maps/public/components/metrics_editor/mask_expression/mask_editor.tsx create mode 100644 x-pack/plugins/maps/public/components/metrics_editor/mask_expression/mask_expression.tsx diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 6c8f719bf28864..8f5ad4869ebe7a 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -188,10 +188,6 @@ export const GEOCENTROID_AGG_NAME = 'gridCentroid'; export const TOP_TERM_PERCENTAGE_SUFFIX = '__percentage'; export const DEFAULT_PERCENTILE = 50; -export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', { - defaultMessage: 'count', -}); - export const COUNT_PROP_NAME = 'doc_count'; export enum STYLE_TYPE { @@ -341,6 +337,11 @@ export enum WIZARD_ID { TMS_LAYER = 'tmsLayer', } +export enum MASK_OPERATOR { + ABOVE = 'ABOVE', + BELOW = 'BELOW', +} + // Maplibre does not provide any feedback when rendering is complete. // Workaround is hard-coded timeout period. export const RENDER_TIMEOUT = 1000; diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index b862558d6a2152..c76bc8f6a6e17e 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -13,6 +13,7 @@ import { SortDirection } from '@kbn/data-plugin/common/search'; import { AGG_TYPE, GRID_RESOLUTION, + MASK_OPERATOR, RENDER_AS, SCALING_TYPES, MVT_FIELD_TYPE, @@ -49,6 +50,10 @@ export type AbstractESSourceDescriptor = AbstractSourceDescriptor & { type AbstractAggDescriptor = { type: AGG_TYPE; label?: string; + mask?: { + operator: MASK_OPERATOR; + value: number; + }; }; export type CountAggDescriptor = AbstractAggDescriptor & { diff --git a/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts b/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts index d881accb1e42fb..6db0d32dbd551f 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts @@ -9,14 +9,17 @@ import { DataView } from '@kbn/data-plugin/common'; import { IField } from '../field'; import { IESAggSource } from '../../sources/es_agg_source'; import { FIELD_ORIGIN } from '../../../../common/constants'; +import { AggDescriptor } from '../../../../common/descriptor_types'; export interface IESAggField extends IField { getValueAggDsl(indexPattern: DataView): unknown | null; getBucketCount(): number; + getMask(): AggDescriptor['mask'] | undefined; } export interface CountAggFieldParams { label?: string; source: IESAggSource; origin: FIELD_ORIGIN; + mask?: AggDescriptor['mask']; } diff --git a/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts index 03ade37c3dbecc..140f605832fcad 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts @@ -14,7 +14,7 @@ import { DataView } from '@kbn/data-plugin/common'; import { IESAggSource } from '../../sources/es_agg_source'; import { IVectorSource } from '../../sources/vector_source'; import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; -import { TileMetaFeature } from '../../../../common/descriptor_types'; +import { AggDescriptor, TileMetaFeature } from '../../../../common/descriptor_types'; import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; import { ESAggTooltipProperty } from '../../tooltips/es_agg_tooltip_property'; import { IESAggField, CountAggFieldParams } from './agg_field_types'; @@ -25,11 +25,13 @@ export class CountAggField implements IESAggField { protected readonly _source: IESAggSource; private readonly _origin: FIELD_ORIGIN; protected readonly _label?: string; + protected readonly _mask?: AggDescriptor['mask']; - constructor({ label, source, origin }: CountAggFieldParams) { + constructor({ label, source, origin, mask }: CountAggFieldParams) { this._source = source; this._origin = origin; this._label = label; + this._mask = mask; } supportsFieldMetaFromEs(): boolean { @@ -131,4 +133,8 @@ export class CountAggField implements IESAggField { pluckRangeFromTileMetaFeature(metaFeature: TileMetaFeature) { return getAggRange(metaFeature, '_count'); } + + getMask() { + return this._mask; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts b/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts index 7e43a2a63658cb..9db4e481b99639 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts @@ -26,6 +26,7 @@ export function esAggFieldsFactory( label: aggDescriptor.label, source, origin, + mask: aggDescriptor.mask, }); } else if (aggDescriptor.type === AGG_TYPE.PERCENTILE) { aggField = new PercentileAggField({ @@ -40,6 +41,7 @@ export function esAggFieldsFactory( : DEFAULT_PERCENTILE, source, origin, + mask: aggDescriptor.mask, }); } else { aggField = new AggField({ @@ -51,6 +53,7 @@ export function esAggFieldsFactory( aggType: aggDescriptor.type, source, origin, + mask: aggDescriptor.mask, }); } diff --git a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts index c6a19f448390ad..568fad59e058b8 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts @@ -105,4 +105,8 @@ export class TopTermPercentageField implements IESAggField { pluckRangeFromTileMetaFeature(metaFeature: TileMetaFeature) { return null; } + + getMask() { + return undefined; + } } diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index 0421fc3b087b5f..69988f0e8a6cbe 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Map as MbMap, VectorTileSource } from '@kbn/mapbox-gl'; +import type { FilterSpecification, Map as MbMap, VectorTileSource } from '@kbn/mapbox-gl'; import { AbstractLayer } from '../layer'; import { HeatmapStyle } from '../../styles/heatmap/heatmap_style'; import { LAYER_TYPE } from '../../../../common/constants'; @@ -21,6 +21,7 @@ import { DataRequestContext } from '../../../actions'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { IMvtVectorSource } from '../../sources/vector_source'; import { getAggsMeta } from '../../util/tile_meta_feature_utils'; +import { Mask } from '../vector_layer/mask'; export class HeatmapLayer extends AbstractLayer { private readonly _style: HeatmapStyle; @@ -186,6 +187,19 @@ export class HeatmapLayer extends AbstractLayer { this.syncVisibilityWithMb(mbMap, heatmapLayerId); mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha()); + + // heatmap can implement mask with filter expression because + // feature-state support is not needed since heatmap layers do not support joins + const maskDescriptor = metricField.getMask(); + if (maskDescriptor) { + const mask = new Mask({ + esAggField: metricField, + isGeometrySourceMvt: true, + ...maskDescriptor, + }); + mbMap.setFilter(heatmapLayerId, mask.getMatchUnmaskedExpression() as FilterSpecification); + } + mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom()); } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts index ee9fdaf410abbc..200c8cad24a4c1 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts @@ -13,7 +13,6 @@ import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style import { IDynamicStyleProperty } from '../../../styles/vector/properties/dynamic_style_property'; import { IStyleProperty } from '../../../styles/vector/properties/style_property'; import { - COUNT_PROP_LABEL, COUNT_PROP_NAME, GRID_RESOLUTION, LAYER_TYPE, @@ -67,7 +66,6 @@ function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle clusterSourceDescriptor.metrics = [ { type: AGG_TYPE.COUNT, - label: COUNT_PROP_LABEL, }, ...documentStyle.getDynamicPropertiesArray().map((dynamicProperty) => { return { @@ -267,9 +265,9 @@ export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLay return [clonedDescriptor]; } - getSource(): IVectorSource { + getSource = () => { return this._isClustered ? this._clusterSource : this._documentSource; - } + }; getSourceForEditing() { // Layer is based on this._documentSource diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts new file mode 100644 index 00000000000000..0b1861fd73397a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { MapGeoJSONFeature } from '@kbn/mapbox-gl'; +import type { IESAggSource } from '../../sources/es_agg_source'; +import type { IESAggField } from '../../fields/agg'; +import { FIELD_ORIGIN, MASK_OPERATOR, MB_LOOKUP_FUNCTION } from '../../../../common/constants'; + +export const BELOW = i18n.translate('xpack.maps.mask.belowLabel', { + defaultMessage: 'below', +}); + +export const ABOVE = i18n.translate('xpack.maps.mask.aboveLabel', { + defaultMessage: 'above', +}); + +export const BUCKETS = i18n.translate('xpack.maps.mask.genericBucketsName', { + defaultMessage: 'buckets', +}); + +const FEATURES = i18n.translate('xpack.maps.mask.genericFeaturesName', { + defaultMessage: 'features', +}); + +const VALUE = i18n.translate('xpack.maps.mask.genericAggLabel', { + defaultMessage: 'value', +}); + +const WHEN = i18n.translate('xpack.maps.mask.when', { + defaultMessage: 'when', +}); + +const WHEN_JOIN_METRIC = i18n.translate('xpack.maps.mask.whenJoinMetric', { + defaultMessage: '{whenLabel} join metric', + values: { + whenLabel: WHEN, + }, +}); + +function getOperatorLabel(operator: MASK_OPERATOR): string { + if (operator === MASK_OPERATOR.BELOW) { + return BELOW; + } + + if (operator === MASK_OPERATOR.ABOVE) { + return ABOVE; + } + + return operator as string; +} + +export function getMaskI18nValue(operator: MASK_OPERATOR, value: number): string { + return `${getOperatorLabel(operator)} ${value}`; +} + +export function getMaskI18nLabel({ + bucketsName, + isJoin, +}: { + bucketsName?: string; + isJoin: boolean; +}): string { + return i18n.translate('xpack.maps.mask.maskLabel', { + defaultMessage: 'Hide {hideNoun}', + values: { + hideNoun: isJoin ? FEATURES : bucketsName ? bucketsName : BUCKETS, + }, + }); +} + +export function getMaskI18nDescription({ + aggLabel, + bucketsName, + isJoin, +}: { + aggLabel?: string; + bucketsName?: string; + isJoin: boolean; +}): string { + return i18n.translate('xpack.maps.mask.maskDescription', { + defaultMessage: '{maskAdverb} {aggLabel} is ', + values: { + aggLabel: aggLabel ? aggLabel : VALUE, + maskAdverb: isJoin ? WHEN_JOIN_METRIC : WHEN, + }, + }); +} + +export class Mask { + private readonly _esAggField: IESAggField; + private readonly _isGeometrySourceMvt: boolean; + private readonly _operator: MASK_OPERATOR; + private readonly _value: number; + + constructor({ + esAggField, + isGeometrySourceMvt, + operator, + value, + }: { + esAggField: IESAggField; + isGeometrySourceMvt: boolean; + operator: MASK_OPERATOR; + value: number; + }) { + this._esAggField = esAggField; + this._isGeometrySourceMvt = isGeometrySourceMvt; + this._operator = operator; + this._value = value; + } + + private _isFeatureState() { + if (this._esAggField.getOrigin() === FIELD_ORIGIN.SOURCE) { + // source fields are stored in properties + return false; + } + + if (!this._isGeometrySourceMvt) { + // For geojson sources, join fields are stored in properties + return false; + } + + // For vector tile sources, it is not possible to add join fields to properties + // so join fields are stored in feature state + return true; + } + + /* + * Returns maplibre expression that matches masked features + */ + getMatchMaskedExpression() { + const comparisionOperator = this._operator === MASK_OPERATOR.BELOW ? '<' : '>'; + const lookup = this._isFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; + return [comparisionOperator, [lookup, this._esAggField.getMbFieldName()], this._value]; + } + + /* + * Returns maplibre expression that matches unmasked features + */ + getMatchUnmaskedExpression() { + const comparisionOperator = this._operator === MASK_OPERATOR.BELOW ? '>=' : '<='; + const lookup = this._isFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; + return [comparisionOperator, [lookup, this._esAggField.getMbFieldName()], this._value]; + } + + getEsAggField() { + return this._esAggField; + } + + getFieldOriginListLabel() { + const source = this._esAggField.getSource(); + const isJoin = this._esAggField.getOrigin() === FIELD_ORIGIN.JOIN; + const maskLabel = getMaskI18nLabel({ + bucketsName: + 'getBucketsName' in (source as IESAggSource) + ? (source as IESAggSource).getBucketsName() + : undefined, + isJoin, + }); + const adverb = isJoin ? WHEN_JOIN_METRIC : WHEN; + + return `${maskLabel} ${adverb}`; + } + + getOperator() { + return this._operator; + } + + getValue() { + return this._value; + } + + isFeatureMasked(feature: MapGeoJSONFeature) { + const featureValue = this._isFeatureState() + ? feature?.state[this._esAggField.getMbFieldName()] + : feature?.properties[this._esAggField.getMbFieldName()]; + if (typeof featureValue !== 'number') { + return false; + } + return this._operator === MASK_OPERATOR.BELOW + ? featureValue < this._value + : featureValue > this._value; + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx index a976837ee2881b..82bb15c19ffca2 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx @@ -25,6 +25,7 @@ import { } from '../../../../../common/descriptor_types'; import { LAYER_TYPE, SOURCE_TYPES } from '../../../../../common/constants'; import { MvtVectorLayer } from './mvt_vector_layer'; +import { ITermJoinSource } from '../../../sources/term_join_source'; const defaultConfig = { urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', @@ -176,6 +177,9 @@ describe('isLayerLoading', () => { getSourceDataRequestId: () => { return 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2'; }, + getRightJoinSource: () => { + return {} as unknown as ITermJoinSource; + }, } as unknown as InnerJoin, ], layerDescriptor: { @@ -212,6 +216,9 @@ describe('isLayerLoading', () => { getSourceDataRequestId: () => { return 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2'; }, + getRightJoinSource: () => { + return {} as unknown as ITermJoinSource; + }, } as unknown as InnerJoin, ], layerDescriptor: { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index dbee11b617dec1..366c9cde6eee6a 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -58,12 +58,14 @@ import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; import { IESSource } from '../../sources/es_source'; import { ITermJoinSource } from '../../sources/term_join_source'; +import type { IESAggSource } from '../../sources/es_agg_source'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { getJoinAggKey } from '../../../../common/get_agg_key'; import { syncBoundsData } from './bounds_data'; import { JoinState } from './types'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { PropertiesMap } from '../../../../common/elasticsearch_util'; +import { Mask } from './mask'; const SUPPORTS_FEATURE_EDITING_REQUEST_ID = 'SUPPORTS_FEATURE_EDITING_REQUEST_ID'; @@ -106,6 +108,7 @@ export interface IVectorLayer extends ILayer { getLeftJoinFields(): Promise; addFeature(geometry: Geometry | Position[]): Promise; deleteFeature(featureId: string): Promise; + getMasks(): Mask[]; } export const noResultsIcon = ; @@ -120,6 +123,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { protected readonly _style: VectorStyle; private readonly _joins: InnerJoin[]; protected readonly _descriptor: VectorLayerDescriptor; + private readonly _masks: Mask[]; static createDescriptor( options: Partial, @@ -163,6 +167,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { customIcons, chartsPaletteServiceGetColor ); + this._masks = this._createMasks(); } async cloneDescriptor(): Promise { @@ -692,6 +697,69 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { return undefined; } + _createMasks() { + const masks: Mask[] = []; + const source = this.getSource(); + if ('getMetricFields' in (source as IESAggSource)) { + const metricFields = (source as IESAggSource).getMetricFields(); + metricFields.forEach((metricField) => { + const maskDescriptor = metricField.getMask(); + if (maskDescriptor) { + masks.push( + new Mask({ + esAggField: metricField, + isGeometrySourceMvt: source.isMvt(), + ...maskDescriptor, + }) + ); + } + }); + } + + this.getValidJoins().forEach((join) => { + const rightSource = join.getRightJoinSource(); + if ('getMetricFields' in (rightSource as unknown as IESAggSource)) { + const metricFields = (rightSource as unknown as IESAggSource).getMetricFields(); + metricFields.forEach((metricField) => { + const maskDescriptor = metricField.getMask(); + if (maskDescriptor) { + masks.push( + new Mask({ + esAggField: metricField, + isGeometrySourceMvt: source.isMvt(), + ...maskDescriptor, + }) + ); + } + }); + } + }); + + return masks; + } + + getMasks() { + return this._masks; + } + + // feature-state is not supported in filter expressions + // https://github.com/mapbox/mapbox-gl-js/issues/8487 + // therefore, masking must be accomplished via setting opacity paint property (hack) + _getAlphaExpression() { + const maskCaseExpressions: unknown[] = []; + this.getMasks().forEach((mask) => { + // case expressions require 2 parts + // 1) condition expression + maskCaseExpressions.push(mask.getMatchMaskedExpression()); + // 2) output. 0 opacity styling "hides" feature + maskCaseExpressions.push(0); + }); + + return maskCaseExpressions.length + ? ['case', ...maskCaseExpressions, this.getAlpha()] + : this.getAlpha(); + } + _setMbPointsProperties( mbMap: MbMap, mvtSourceLayer?: string, @@ -759,13 +827,13 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { if (this.getCurrentStyle().arePointsSymbolizedAsCircles()) { this.getCurrentStyle().setMBPaintPropertiesForPoints({ - alpha: this.getAlpha(), + alpha: this._getAlphaExpression(), mbMap, pointLayerId: markerLayerId, }); } else { this.getCurrentStyle().setMBSymbolPropertiesForPoints({ - alpha: this.getAlpha(), + alpha: this._getAlphaExpression(), mbMap, symbolLayerId: markerLayerId, }); @@ -811,7 +879,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { } this.getCurrentStyle().setMBPaintProperties({ - alpha: this.getAlpha(), + alpha: this._getAlphaExpression(), mbMap, fillLayerId, lineLayerId, @@ -865,7 +933,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { } this.getCurrentStyle().setMBPropertiesForLabelText({ - alpha: this.getAlpha(), + alpha: this._getAlphaExpression(), mbMap, textLayerId: labelLayerId, }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts index fa7f329beb97af..dda3026ddd4ef3 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts @@ -11,11 +11,13 @@ import { DataView } from '@kbn/data-plugin/common'; import type { IESAggSource } from './types'; import { AbstractESSource } from '../es_source'; import { esAggFieldsFactory, IESAggField } from '../../fields/agg'; -import { AGG_TYPE, COUNT_PROP_LABEL, FIELD_ORIGIN } from '../../../../common/constants'; +import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; import { getSourceAggKey } from '../../../../common/get_agg_key'; import { AbstractESAggSourceDescriptor, AggDescriptor } from '../../../../common/descriptor_types'; import { IField } from '../../fields/field'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; +import { getAggDisplayName } from './get_agg_display_name'; +import { BUCKETS } from '../../layers/vector_layer/mask'; export const DEFAULT_METRIC = { type: AGG_TYPE.COUNT }; @@ -46,6 +48,10 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE } } + getBucketsName() { + return BUCKETS; + } + getFieldByName(fieldName: string): IField | null { return this.getMetricFieldForName(fieldName); } @@ -83,14 +89,14 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE async getAggLabel(aggType: AGG_TYPE, fieldLabel: string): Promise { switch (aggType) { case AGG_TYPE.COUNT: - return COUNT_PROP_LABEL; + return getAggDisplayName(aggType); case AGG_TYPE.TERMS: return i18n.translate('xpack.maps.source.esAggSource.topTermLabel', { - defaultMessage: `Top {fieldLabel}`, + defaultMessage: `top {fieldLabel}`, values: { fieldLabel }, }); default: - return `${aggType} ${fieldLabel}`; + return `${getAggDisplayName(aggType)} ${fieldLabel}`; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/get_agg_display_name.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/get_agg_display_name.ts new file mode 100644 index 00000000000000..516f6448fb6296 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/get_agg_display_name.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { AGG_TYPE } from '../../../../common/constants'; + +export function getAggDisplayName(aggType: AGG_TYPE): string { + switch (aggType) { + case AGG_TYPE.AVG: + return i18n.translate('xpack.maps.aggType.averageLabel', { + defaultMessage: 'average', + }); + case AGG_TYPE.COUNT: + return i18n.translate('xpack.maps.aggType.countLabel', { + defaultMessage: 'count', + }); + case AGG_TYPE.MAX: + return i18n.translate('xpack.maps.aggType.maximumLabel', { + defaultMessage: 'max', + }); + case AGG_TYPE.MIN: + return i18n.translate('xpack.maps.aggType.minimumLabel', { + defaultMessage: 'min', + }); + case AGG_TYPE.PERCENTILE: + return i18n.translate('xpack.maps.aggType.percentileLabel', { + defaultMessage: 'percentile', + }); + case AGG_TYPE.SUM: + return i18n.translate('xpack.maps.aggType.sumLabel', { + defaultMessage: 'sum', + }); + case AGG_TYPE.TERMS: + return i18n.translate('xpack.maps.aggType.topTermLabel', { + defaultMessage: 'top term', + }); + case AGG_TYPE.UNIQUE_COUNT: + return i18n.translate('xpack.maps.aggType.cardinalityTermLabel', { + defaultMessage: 'unique count', + }); + default: + return aggType; + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts index 033d937aa93912..80bc74bd4ceb48 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts @@ -7,3 +7,4 @@ export type { IESAggSource } from './types'; export { AbstractESAggSource, DEFAULT_METRIC } from './es_agg_source'; +export { getAggDisplayName } from './get_agg_display_name'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/types.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/types.ts index d9cb6fcd95a108..697ae8a1a606de 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/types.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/types.ts @@ -13,6 +13,12 @@ import { IESAggField } from '../../fields/agg'; export interface IESAggSource extends IESSource { getAggKey(aggType: AGG_TYPE, fieldName: string): string; getAggLabel(aggType: AGG_TYPE, fieldLabel: string): Promise; + + /* + * Returns human readable name describing buckets, like "clusters" or "grids" + */ + getBucketsName(): string; + getMetricFields(): IESAggField[]; getMetricFieldForName(fieldName: string): IESAggField | null; getValueAggsDsl(indexPattern: DataView): { [key: string]: unknown }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap index 79b8c4ffc98088..fe50d54ca55354 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap @@ -19,7 +19,9 @@ exports[`source editor geo_grid_source should not allow editing multiple metrics /> { async function onChange(...sourceChanges: OnSourceChangeArgs[]) { sourceEditorArgs.onChange(...sourceChanges); @@ -129,6 +147,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo } return ( ({ })); const defaultProps = { + bucketsName: 'clusters', currentLayerType: LAYER_TYPE.GEOJSON_VECTOR, geoFieldName: 'myLocation', indexPatternId: 'foobar', diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx index 3268992c7f2b63..d69a97d09dd477 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx @@ -6,7 +6,6 @@ */ import React, { Fragment, Component } from 'react'; - import { v4 as uuidv4 } from 'uuid'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiPanel, EuiSpacer, EuiComboBoxOptionOption, EuiTitle } from '@elastic/eui'; @@ -25,6 +24,7 @@ import { clustersTitle, heatmapTitle } from './es_geo_grid_source'; import { isMvt } from './is_mvt'; interface Props { + bucketsName: string; currentLayerType?: string; geoFieldName: string; indexPatternId: string; @@ -148,6 +148,8 @@ export class UpdateSourceEditor extends Component { { ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.tsx similarity index 74% rename from x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.tsx index 856504c51865e1..c36cc3d49089ae 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.tsx @@ -6,17 +6,31 @@ */ import React, { Component, Fragment } from 'react'; - -import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters'; -import { MetricsEditor } from '../../../components/metrics_editor'; -import { getIndexPatternService } from '../../../kibana_services'; +import type { DataViewField } from '@kbn/data-plugin/common'; import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { indexPatterns } from '@kbn/data-plugin/public'; +import { MetricsEditor } from '../../../components/metrics_editor'; +import { getIndexPatternService } from '../../../kibana_services'; +import type { AggDescriptor } from '../../../../common/descriptor_types'; +import type { OnSourceChangeArgs } from '../source'; + +interface Props { + bucketsName: string; + indexPatternId: string; + metrics: AggDescriptor[]; + onChange: (...args: OnSourceChangeArgs[]) => void; +} + +interface State { + fields: DataViewField[]; +} + +export class UpdateSourceEditor extends Component { + private _isMounted: boolean = false; -export class UpdateSourceEditor extends Component { state = { - fields: null, + fields: [], }; componentDidMount() { @@ -33,11 +47,6 @@ export class UpdateSourceEditor extends Component { try { indexPattern = await getIndexPatternService().get(this.props.indexPatternId); } catch (err) { - if (this._isMounted) { - this.setState({ - loadError: getDataViewNotFoundMessage(this.props.indexPatternId), - }); - } return; } @@ -50,7 +59,7 @@ export class UpdateSourceEditor extends Component { }); } - _onMetricsChange = (metrics) => { + _onMetricsChange = (metrics: AggDescriptor[]) => { this.props.onChange({ propName: 'metrics', value: metrics }); }; @@ -69,6 +78,8 @@ export class UpdateSourceEditor extends Component { { }); const metrics = source.getMetricFields(); expect(metrics[0].getName()).toEqual('__kbnjoin__count__1234'); - expect(await metrics[0].getLabel()).toEqual('Count of foobar'); + expect(await metrics[0].getLabel()).toEqual('count of foobar'); }); it('should override name and label of sum metric', async () => { @@ -51,7 +51,7 @@ describe('getMetricFields', () => { expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed__1234'); expect(await metrics[0].getLabel()).toEqual('my custom label'); expect(metrics[1].getName()).toEqual('__kbnjoin__count__1234'); - expect(await metrics[1].getLabel()).toEqual('Count of foobar'); + expect(await metrics[1].getLabel()).toEqual('count of foobar'); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index 55404547021147..4c7793e1b01cb2 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -31,6 +31,7 @@ import { import { PropertiesMap } from '../../../../common/elasticsearch_util'; import { isValidStringConfig } from '../../util/valid_string_config'; import { ITermJoinSource } from '../term_join_source'; +import type { IESAggSource } from '../es_agg_source'; import { IField } from '../../fields/field'; import { mergeExecutionContext } from '../execution_context_utils'; @@ -52,7 +53,7 @@ export function extractPropertiesMap(rawEsData: any, countPropertyName: string): return propertiesMap; } -export class ESTermSource extends AbstractESAggSource implements ITermJoinSource { +export class ESTermSource extends AbstractESAggSource implements ITermJoinSource, IESAggSource { static type = SOURCE_TYPES.ES_TERM_SOURCE; static createDescriptor(descriptor: Partial): ESTermSourceDescriptor { @@ -115,7 +116,7 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource } return aggType === AGG_TYPE.COUNT ? i18n.translate('xpack.maps.source.esJoin.countLabel', { - defaultMessage: `Count of {indexPatternLabel}`, + defaultMessage: `count of {indexPatternLabel}`, values: { indexPatternLabel }, }) : super.getAggLabel(aggType, fieldLabel); diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx index 390e48408d7478..027e4dc29c58cd 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx @@ -5,13 +5,15 @@ * 2.0. */ -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import { i18n } from '@kbn/i18n'; import { ColorGradient } from './color_gradient'; import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row'; import { HEATMAP_COLOR_RAMP_LABEL } from '../heatmap_constants'; -import { IField } from '../../../../fields/field'; +import type { IField } from '../../../../fields/field'; +import type { IESAggField } from '../../../../fields/agg'; +import { MaskLegend } from '../../../vector/components/legend/mask_legend'; interface Props { colorRampName: string; @@ -47,7 +49,7 @@ export class HeatmapLegend extends Component { } render() { - return ( + const metricLegend = ( } minLabel={i18n.translate('xpack.maps.heatmapLegend.coldLabel', { @@ -61,5 +63,28 @@ export class HeatmapLegend extends Component { invert={false} /> ); + + let maskLegend: ReactNode | undefined; + if ('getMask' in (this.props.field as IESAggField)) { + const mask = (this.props.field as IESAggField).getMask(); + if (mask) { + maskLegend = ( + + ); + } + } + + return maskLegend ? ( + <> + {maskLegend} + {metricLegend} + + ) : ( + metricLegend + ); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/mask_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/mask_legend.tsx new file mode 100644 index 00000000000000..4ffb7ba5a18344 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/mask_legend.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { FIELD_ORIGIN, MASK_OPERATOR } from '../../../../../../common/constants'; +import type { IESAggField } from '../../../../fields/agg'; +import type { IESAggSource } from '../../../../sources/es_agg_source'; +import { + getMaskI18nDescription, + getMaskI18nLabel, + getMaskI18nValue, +} from '../../../../layers/vector_layer/mask'; + +interface Props { + esAggField: IESAggField; + onlyShowLabelAndValue?: boolean; + operator: MASK_OPERATOR; + value: number; +} + +interface State { + aggLabel?: string; +} + +export class MaskLegend extends Component { + private _isMounted = false; + + state: State = {}; + + componentDidMount() { + this._isMounted = true; + this._loadAggLabel(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + this._loadAggLabel(); + } + + _loadAggLabel = async () => { + const aggLabel = await this.props.esAggField.getLabel(); + if (this._isMounted && aggLabel !== this.state.aggLabel) { + this.setState({ aggLabel }); + } + }; + + _getBucketsName() { + const source = this.props.esAggField.getSource(); + return 'getBucketsName' in (source as IESAggSource) + ? (source as IESAggSource).getBucketsName() + : undefined; + } + + _getPrefix() { + if (this.props.onlyShowLabelAndValue) { + return i18n.translate('xpack.maps.maskLegend.is', { + defaultMessage: '{aggLabel} is', + values: { + aggLabel: this.state.aggLabel, + }, + }); + } + + const isJoin = this.props.esAggField.getOrigin() === FIELD_ORIGIN.JOIN; + const maskLabel = getMaskI18nLabel({ + bucketsName: this._getBucketsName(), + isJoin, + }); + const maskDescription = getMaskI18nDescription({ + aggLabel: this.state.aggLabel, + isJoin, + }); + return `${maskLabel} ${maskDescription}`; + } + + render() { + return ( + + + {`${this._getPrefix()} `} + {getMaskI18nValue(this.props.operator, this.props.value)} + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx index 2d282a4b530cb1..60bcd05c9f7381 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx @@ -6,17 +6,30 @@ */ import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { FIELD_ORIGIN } from '../../../../../../common/constants'; +import { Mask } from '../../../../layers/vector_layer/mask'; import { IStyleProperty } from '../../properties/style_property'; +import { MaskLegend } from './mask_legend'; interface Props { isLinesOnly: boolean; isPointsOnly: boolean; + masks: Mask[]; styles: Array>; symbolId?: string; svg?: string; } -export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId, svg }: Props) { +export function VectorStyleLegend({ + isLinesOnly, + isPointsOnly, + masks, + styles, + symbolId, + svg, +}: Props) { const legendRows = []; for (let i = 0; i < styles.length; i++) { @@ -34,5 +47,55 @@ export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId, ); } - return <>{legendRows}; + function renderMasksByFieldOrigin(fieldOrigin: FIELD_ORIGIN) { + const masksByFieldOrigin = masks.filter( + (mask) => mask.getEsAggField().getOrigin() === fieldOrigin + ); + if (masksByFieldOrigin.length === 0) { + return null; + } + + if (masksByFieldOrigin.length === 1) { + const mask = masksByFieldOrigin[0]; + return ( + + ); + } + + return ( + <> + + {masksByFieldOrigin[0].getFieldOriginListLabel()} + +
    + {masksByFieldOrigin.map((mask) => ( +
  • + +
  • + ))} +
+ + ); + } + + return ( + <> + {renderMasksByFieldOrigin(FIELD_ORIGIN.SOURCE)} + {renderMasksByFieldOrigin(FIELD_ORIGIN.JOIN)} + {legendRows} + + ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx index 6daa8cf84afaa3..f1a55b571e27dd 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx @@ -51,7 +51,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { - syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'circle-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'circle-opacity', alpha); } - syncFillColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncFillColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'fill-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'fill-opacity', alpha); } @@ -28,17 +28,17 @@ export class StaticColorProperty extends StaticStyleProperty mbMap.setPaintProperty(mbLayerId, 'icon-halo-color', this._options.color); } - syncLineColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncLineColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'line-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha); } - syncCircleStrokeWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncCircleStrokeWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'circle-stroke-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'circle-stroke-opacity', alpha); } - syncLabelColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncLabelColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'text-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'text-opacity', alpha); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 3b64d0960628c0..31c06728d81121 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -130,7 +130,7 @@ export interface IVectorStyle extends IStyle { fillLayerId, lineLayerId, }: { - alpha: number; + alpha: unknown; mbMap: MbMap; fillLayerId: string; lineLayerId: string; @@ -140,7 +140,7 @@ export interface IVectorStyle extends IStyle { mbMap, pointLayerId, }: { - alpha: number; + alpha: unknown; mbMap: MbMap; pointLayerId: string; }) => void; @@ -149,7 +149,7 @@ export interface IVectorStyle extends IStyle { mbMap, textLayerId, }: { - alpha: number; + alpha: unknown; mbMap: MbMap; textLayerId: string; }) => void; @@ -158,7 +158,7 @@ export interface IVectorStyle extends IStyle { symbolLayerId, alpha, }: { - alpha: number; + alpha: unknown; mbMap: MbMap; symbolLayerId: string; }) => void; @@ -730,6 +730,7 @@ export class VectorStyle implements IVectorStyle { return ( void; + onClose: () => void; +} + +interface State { + operator: MASK_OPERATOR; + value: number | string; +} + +export class MaskEditor extends Component { + constructor(props: Props) { + super(props); + this.state = { + operator: + this.props.metric.mask !== undefined + ? this.props.metric.mask.operator + : MASK_OPERATOR.BELOW, + value: this.props.metric.mask !== undefined ? this.props.metric.mask.value : '', + }; + } + + _onSet = () => { + if (this._isValueInValid()) { + return; + } + + this.props.onChange({ + ...this.props.metric, + mask: { + operator: this.state.operator, + value: this.state.value as number, + }, + }); + this.props.onClose(); + }; + + _onClear = () => { + const newMetric = { + ...this.props.metric, + }; + delete newMetric.mask; + this.props.onChange(newMetric); + this.props.onClose(); + }; + + _onOperatorChange = (e: ChangeEvent) => { + this.setState({ + operator: e.target.value as MASK_OPERATOR, + }); + }; + + _onValueChange = (evt: ChangeEvent) => { + const sanitizedValue = parseFloat(evt.target.value); + this.setState({ + value: isNaN(sanitizedValue) ? evt.target.value : sanitizedValue, + }); + }; + + _hasChanges() { + return ( + this.props.metric.mask === undefined || + this.props.metric.mask.operator !== this.state.operator || + this.props.metric.mask.value !== this.state.value + ); + } + + _isValueInValid() { + return typeof this.state.value === 'string'; + } + + _renderForm() { + return ( + + + + + + + + + + + + + ); + } + + _renderFooter() { + return ( + + + + + {panelStrings.close} + + + + + {panelStrings.clear} + + + + + {panelStrings.apply} + + + + + ); + } + + render() { + return ( + <> + {this._renderForm()} + + {this._renderFooter()} + + ); + } +} diff --git a/x-pack/plugins/maps/public/components/metrics_editor/mask_expression/mask_expression.tsx b/x-pack/plugins/maps/public/components/metrics_editor/mask_expression/mask_expression.tsx new file mode 100644 index 00000000000000..e05fdc6cdf2d51 --- /dev/null +++ b/x-pack/plugins/maps/public/components/metrics_editor/mask_expression/mask_expression.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { EuiExpression, EuiPopover } from '@elastic/eui'; +import { DataViewField } from '@kbn/data-views-plugin/public'; +import { AGG_TYPE } from '../../../../common/constants'; +import { AggDescriptor, FieldedAggDescriptor } from '../../../../common/descriptor_types'; +import { MaskEditor } from './mask_editor'; +import { getAggDisplayName } from '../../../classes/sources/es_agg_source'; +import { + getMaskI18nDescription, + getMaskI18nValue, +} from '../../../classes/layers/vector_layer/mask'; + +interface Props { + fields: DataViewField[]; + isJoin: boolean; + metric: AggDescriptor; + onChange: (metric: AggDescriptor) => void; +} + +interface State { + isPopoverOpen: boolean; +} + +export class MaskExpression extends Component { + state: State = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + _getMaskExpressionValue() { + return this.props.metric.mask === undefined + ? '...' + : getMaskI18nValue(this.props.metric.mask.operator, this.props.metric.mask.value); + } + + _getAggLabel() { + const aggDisplayName = getAggDisplayName(this.props.metric.type); + if (this.props.metric.type === AGG_TYPE.COUNT || this.props.metric.field === undefined) { + return aggDisplayName; + } + + const targetField = this.props.fields.find( + (field) => field.name === (this.props.metric as FieldedAggDescriptor).field + ); + const fieldDisplayName = targetField?.displayName + ? targetField?.displayName + : this.props.metric.field; + return `${aggDisplayName} ${fieldDisplayName}`; + } + + render() { + // masks only supported for numerical metrics + if (this.props.metric.type === AGG_TYPE.TERMS) { + return null; + } + + return ( + + } + isOpen={this.state.isPopoverOpen} + closePopover={this._closePopover} + panelPaddingSize="s" + anchorPosition="downCenter" + repositionOnScroll={true} + > + + + ); + } +} diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx index 26cf6d5313821b..5042cb4ffa9c7f 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx @@ -18,6 +18,8 @@ import { AggDescriptor } from '../../../common/descriptor_types'; import { AGG_TYPE, DEFAULT_PERCENTILE } from '../../../common/constants'; import { getTermsFields } from '../../index_pattern_util'; import { ValidatedNumberInput } from '../validated_number_input'; +import { getMaskI18nLabel } from '../../classes/layers/vector_layer/mask'; +import { MaskExpression } from './mask_expression'; function filterFieldsForAgg(fields: DataViewField[], aggType: AGG_TYPE) { if (!fields) { @@ -43,6 +45,8 @@ function filterFieldsForAgg(fields: DataViewField[], aggType: AGG_TYPE) { } interface Props { + bucketsName?: string; + isJoin: boolean; metric: AggDescriptor; fields: DataViewField[]; onChange: (metric: AggDescriptor) => void; @@ -52,7 +56,9 @@ interface Props { } export function MetricEditor({ + bucketsName, fields, + isJoin, metricsFilter, metric, onChange, @@ -64,6 +70,8 @@ export function MetricEditor({ return; } + // Intentionally not adding mask. + // Changing aggregation likely changes value range so keeping old mask does not seem relevent const descriptor = { type: metricAggregationType, label: metric.label, @@ -93,6 +101,8 @@ export function MetricEditor({ if (!fieldName || metric.type === AGG_TYPE.COUNT) { return; } + // Intentionally not adding mask. + // Changing field likely changes value range so keeping old mask does not seem relevent onChange({ label: metric.label, type: metric.type, @@ -223,6 +233,11 @@ export function MetricEditor({ {fieldSelect} {percentileSelect} {labelInput} + + + + + {removeButton} ); diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx index e8dd63fe934e59..88c9b83bc54a49 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx @@ -9,54 +9,39 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; import { AGG_TYPE } from '../../../common/constants'; +import { getAggDisplayName } from '../../classes/sources/es_agg_source'; const AGG_OPTIONS = [ { - label: i18n.translate('xpack.maps.metricSelect.averageDropDownOptionLabel', { - defaultMessage: 'Average', - }), + label: getAggDisplayName(AGG_TYPE.AVG), value: AGG_TYPE.AVG, }, { - label: i18n.translate('xpack.maps.metricSelect.countDropDownOptionLabel', { - defaultMessage: 'Count', - }), + label: getAggDisplayName(AGG_TYPE.COUNT), value: AGG_TYPE.COUNT, }, { - label: i18n.translate('xpack.maps.metricSelect.maxDropDownOptionLabel', { - defaultMessage: 'Max', - }), + label: getAggDisplayName(AGG_TYPE.MAX), value: AGG_TYPE.MAX, }, { - label: i18n.translate('xpack.maps.metricSelect.minDropDownOptionLabel', { - defaultMessage: 'Min', - }), + label: getAggDisplayName(AGG_TYPE.MIN), value: AGG_TYPE.MIN, }, { - label: i18n.translate('xpack.maps.metricSelect.percentileDropDownOptionLabel', { - defaultMessage: 'Percentile', - }), + label: getAggDisplayName(AGG_TYPE.PERCENTILE), value: AGG_TYPE.PERCENTILE, }, { - label: i18n.translate('xpack.maps.metricSelect.sumDropDownOptionLabel', { - defaultMessage: 'Sum', - }), + label: getAggDisplayName(AGG_TYPE.SUM), value: AGG_TYPE.SUM, }, { - label: i18n.translate('xpack.maps.metricSelect.termsDropDownOptionLabel', { - defaultMessage: 'Top term', - }), + label: getAggDisplayName(AGG_TYPE.TERMS), value: AGG_TYPE.TERMS, }, { - label: i18n.translate('xpack.maps.metricSelect.cardinalityDropDownOptionLabel', { - defaultMessage: 'Unique count', - }), + label: getAggDisplayName(AGG_TYPE.UNIQUE_COUNT), value: AGG_TYPE.UNIQUE_COUNT, }, ]; diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx index 66fed40936b794..5aaf1369efe814 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx @@ -20,6 +20,7 @@ const defaultProps = { fields: [], onChange: () => {}, allowMultipleMetrics: true, + isJoin: false, }; test('should render metrics editor', () => { diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx index b38e20b40d990e..a18608b9631c28 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx @@ -22,6 +22,8 @@ export function isMetricValid(aggDescriptor: AggDescriptor) { interface Props { allowMultipleMetrics: boolean; + bucketsName?: string; + isJoin: boolean; metrics: AggDescriptor[]; fields: DataViewField[]; onChange: (metrics: AggDescriptor[]) => void; @@ -81,6 +83,8 @@ export class MetricsEditor extends Component { return (
{ metrics={this.props.metrics} onChange={this.props.onChange} allowMultipleMetrics={true} + isJoin={true} /> ); }; diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap index cb496311b3d1c6..46ddc7f58eb783 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap @@ -76,11 +76,7 @@ exports[`Should render edit form row when attribution not provided 1`] = ` onClick={[Function]} size="xs" > - + Clear
diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx index bdab63e1029e76..38ac1959045920 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { EuiButtonEmpty, EuiLink, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { Attribution } from '../../../../common/descriptor_types'; import { ILayer } from '../../../classes/layers/layer'; import { AttributionPopover } from './attribution_popover'; +import { panelStrings } from '../../panel_strings'; interface Props { layer: ILayer; @@ -65,9 +65,7 @@ export function AttributionFormRow(props: Props) { defaultMessage: 'Edit attribution', } )} - popoverButtonLabel={i18n.translate('xpack.maps.attribution.editBtnLabel', { - defaultMessage: 'Edit', - })} + popoverButtonLabel={panelStrings.edit} label={layerDescriptor.attribution.label} url={layerDescriptor.attribution.url} /> @@ -83,10 +81,7 @@ export function AttributionFormRow(props: Props) { defaultMessage: 'Clear attribution', })} > - + {panelStrings.clear}
diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx index 0371b68c85a3be..530b3cce20f000 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx @@ -20,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { Attribution } from '../../../../common/descriptor_types'; +import { panelStrings } from '../../panel_strings'; interface Props { onChange: (attribution: Attribution) => void; @@ -128,7 +129,7 @@ export class AttributionPopover extends Component { onClick={this._onApply} size="s" > - + {panelStrings.apply} diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx index 8d399f19a765c2..02b9048e93b867 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx @@ -35,7 +35,7 @@ export function StyleSettings({ layer, updateStyleDescriptor, updateCustomIcons
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx index edd27d2d1edb1c..9074fbd8fbeaf5 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx @@ -70,6 +70,9 @@ const mockLayer = { }, } as unknown as IVectorSource; }, + getMasks: () => { + return []; + }, } as unknown as IVectorLayer; const mockMbMapHandlers: { [key: string]: (event?: MapMouseEvent) => void } = {}; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx index 1c14d11eb1f964..75e41464fd0f86 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx @@ -184,6 +184,15 @@ export class TooltipControl extends Component { continue; } + // masking must use paint property "opacity" to hide features in order to support feature state + // therefore, there is no way to remove masked features with queryRenderedFeatures + // masked features must be removed via manual filtering + const masks = layer.getMasks(); + const maskHiddingFeature = masks.find((mask) => mask.isFeatureMasked(mbFeature)); + if (maskHiddingFeature) { + continue; + } + const featureId = layer.getFeatureId(mbFeature); if (featureId === undefined) { continue; diff --git a/x-pack/plugins/maps/public/connected_components/panel_strings.ts b/x-pack/plugins/maps/public/connected_components/panel_strings.ts index f7f7278138e1e7..f4eb5e871a3fe2 100644 --- a/x-pack/plugins/maps/public/connected_components/panel_strings.ts +++ b/x-pack/plugins/maps/public/connected_components/panel_strings.ts @@ -8,12 +8,21 @@ import { i18n } from '@kbn/i18n'; export const panelStrings = { + apply: i18n.translate('xpack.maps.panel.applyLabel', { + defaultMessage: 'Apply', + }), + clear: i18n.translate('xpack.maps.panel.clearLabel', { + defaultMessage: 'Clear', + }), close: i18n.translate('xpack.maps.panel.closeLabel', { defaultMessage: 'Close', }), discardChanges: i18n.translate('xpack.maps.panel.discardChangesLabel', { defaultMessage: 'Discard changes', }), + edit: i18n.translate('xpack.maps.panel.editLabel', { + defaultMessage: 'Edit', + }), keepChanges: i18n.translate('xpack.maps.panel.keepChangesLabel', { defaultMessage: 'Keep changes', }), diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 58305363bb55e0..854795940bbbff 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -20315,15 +20315,11 @@ "xpack.maps.addLayerPanel.addLayer": "Ajouter un calque", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "Changer de calque", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "Annuler", - "xpack.maps.aggs.defaultCountLabel": "compte", "xpack.maps.attribution.addBtnAriaLabel": "Ajouter une attribution", "xpack.maps.attribution.addBtnLabel": "Ajouter une attribution", - "xpack.maps.attribution.applyBtnLabel": "Appliquer", "xpack.maps.attribution.attributionFormLabel": "Attribution", "xpack.maps.attribution.clearBtnAriaLabel": "Effacer l'attribution", - "xpack.maps.attribution.clearBtnLabel": "Effacer", "xpack.maps.attribution.editBtnAriaLabel": "Modifier l'attribution", - "xpack.maps.attribution.editBtnLabel": "Modifier", "xpack.maps.attribution.labelFieldLabel": "Étiquette", "xpack.maps.attribution.urlLabel": "Lien", "xpack.maps.badge.readOnly.text": "Lecture seule", @@ -20595,15 +20591,7 @@ "xpack.maps.metricsEditor.selectFieldLabel": "Champ", "xpack.maps.metricsEditor.selectFieldPlaceholder": "Sélectionner un champ", "xpack.maps.metricsEditor.selectPercentileLabel": "Centile", - "xpack.maps.metricSelect.averageDropDownOptionLabel": "Moyenne", - "xpack.maps.metricSelect.cardinalityDropDownOptionLabel": "Compte unique", - "xpack.maps.metricSelect.countDropDownOptionLabel": "Décompte", - "xpack.maps.metricSelect.maxDropDownOptionLabel": "Max.", - "xpack.maps.metricSelect.minDropDownOptionLabel": "Min.", - "xpack.maps.metricSelect.percentileDropDownOptionLabel": "Centile", "xpack.maps.metricSelect.selectAggregationPlaceholder": "Sélectionner une agrégation", - "xpack.maps.metricSelect.sumDropDownOptionLabel": "Somme", - "xpack.maps.metricSelect.termsDropDownOptionLabel": "Premier terme", "xpack.maps.mvtSource.addFieldLabel": "Ajouter", "xpack.maps.mvtSource.fieldPlaceholderText": "Nom du champ", "xpack.maps.mvtSource.numberFieldLabel": "numéro", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a17220cadd2d2a..23845c19230247 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20315,15 +20315,11 @@ "xpack.maps.addLayerPanel.addLayer": "レイヤーを追加", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "レイヤーを変更", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル", - "xpack.maps.aggs.defaultCountLabel": "カウント", "xpack.maps.attribution.addBtnAriaLabel": "属性を追加", "xpack.maps.attribution.addBtnLabel": "属性を追加", - "xpack.maps.attribution.applyBtnLabel": "適用", "xpack.maps.attribution.attributionFormLabel": "属性", "xpack.maps.attribution.clearBtnAriaLabel": "属性を消去", - "xpack.maps.attribution.clearBtnLabel": "クリア", "xpack.maps.attribution.editBtnAriaLabel": "属性を編集", - "xpack.maps.attribution.editBtnLabel": "編集", "xpack.maps.attribution.labelFieldLabel": "ラベル", "xpack.maps.attribution.urlLabel": "リンク", "xpack.maps.badge.readOnly.text": "読み取り専用", @@ -20595,15 +20591,7 @@ "xpack.maps.metricsEditor.selectFieldLabel": "フィールド", "xpack.maps.metricsEditor.selectFieldPlaceholder": "フィールドを選択", "xpack.maps.metricsEditor.selectPercentileLabel": "パーセンタイル", - "xpack.maps.metricSelect.averageDropDownOptionLabel": "平均", - "xpack.maps.metricSelect.cardinalityDropDownOptionLabel": "ユニークカウント", - "xpack.maps.metricSelect.countDropDownOptionLabel": "カウント", - "xpack.maps.metricSelect.maxDropDownOptionLabel": "最高", - "xpack.maps.metricSelect.minDropDownOptionLabel": "最低", - "xpack.maps.metricSelect.percentileDropDownOptionLabel": "パーセンタイル", "xpack.maps.metricSelect.selectAggregationPlaceholder": "集約を選択", - "xpack.maps.metricSelect.sumDropDownOptionLabel": "合計", - "xpack.maps.metricSelect.termsDropDownOptionLabel": "トップ用語", "xpack.maps.mvtSource.addFieldLabel": "追加", "xpack.maps.mvtSource.fieldPlaceholderText": "フィールド名", "xpack.maps.mvtSource.numberFieldLabel": "数字", @@ -23391,7 +23379,7 @@ "xpack.ml.ruleEditor.scopeSection.noPermissionToViewFilterListsTitle": "フィルターリストを表示するパーミッションがありません", "xpack.ml.ruleEditor.scopeSection.scopeTitle": "範囲", "xpack.ml.ruleEditor.selectRuleAction.createRuleLinkText": "ルールを作成", - "xpack.ml.ruleEditor.selectRuleAction.orText": "OR ", + "xpack.ml.ruleEditor.selectRuleAction.orText": "OR ", "xpack.ml.ruleEditor.typicalAppliesTypeText": "通常", "xpack.ml.sampleDataLinkLabel": "ML ジョブ", "xpack.ml.selectDataViewLabel": "データビューを選択", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 057233f627acec..bd291abcea65bd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20315,15 +20315,11 @@ "xpack.maps.addLayerPanel.addLayer": "添加图层", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "更改图层", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "取消", - "xpack.maps.aggs.defaultCountLabel": "计数", "xpack.maps.attribution.addBtnAriaLabel": "添加归因", "xpack.maps.attribution.addBtnLabel": "添加归因", - "xpack.maps.attribution.applyBtnLabel": "应用", "xpack.maps.attribution.attributionFormLabel": "归因", "xpack.maps.attribution.clearBtnAriaLabel": "清除归因", - "xpack.maps.attribution.clearBtnLabel": "清除", "xpack.maps.attribution.editBtnAriaLabel": "编辑归因", - "xpack.maps.attribution.editBtnLabel": "编辑", "xpack.maps.attribution.labelFieldLabel": "标签", "xpack.maps.attribution.urlLabel": "链接", "xpack.maps.badge.readOnly.text": "只读", @@ -20595,15 +20591,7 @@ "xpack.maps.metricsEditor.selectFieldLabel": "字段", "xpack.maps.metricsEditor.selectFieldPlaceholder": "选择字段", "xpack.maps.metricsEditor.selectPercentileLabel": "百分位数", - "xpack.maps.metricSelect.averageDropDownOptionLabel": "平均值", - "xpack.maps.metricSelect.cardinalityDropDownOptionLabel": "唯一计数", - "xpack.maps.metricSelect.countDropDownOptionLabel": "计数", - "xpack.maps.metricSelect.maxDropDownOptionLabel": "最大值", - "xpack.maps.metricSelect.minDropDownOptionLabel": "最小值", - "xpack.maps.metricSelect.percentileDropDownOptionLabel": "百分位数", "xpack.maps.metricSelect.selectAggregationPlaceholder": "选择聚合", - "xpack.maps.metricSelect.sumDropDownOptionLabel": "求和", - "xpack.maps.metricSelect.termsDropDownOptionLabel": "热门词", "xpack.maps.mvtSource.addFieldLabel": "添加", "xpack.maps.mvtSource.fieldPlaceholderText": "字段名称", "xpack.maps.mvtSource.numberFieldLabel": "数字", @@ -23403,7 +23391,7 @@ "xpack.ml.ruleEditor.scopeSection.noPermissionToViewFilterListsTitle": "您无权查看筛选列表", "xpack.ml.ruleEditor.scopeSection.scopeTitle": "范围", "xpack.ml.ruleEditor.selectRuleAction.createRuleLinkText": "创建规则", - "xpack.ml.ruleEditor.selectRuleAction.orText": "或 ", + "xpack.ml.ruleEditor.selectRuleAction.orText": "或 ", "xpack.ml.ruleEditor.typicalAppliesTypeText": "典型", "xpack.ml.sampleDataLinkLabel": "ML 作业", "xpack.ml.selectDataViewLabel": "选择数据视图", From 1bbaa5a62f7ef8a408a9e205e7a6848ecba8ef9c Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Sat, 22 Apr 2023 07:34:33 +0200 Subject: [PATCH 21/65] [TSDB][Discover] Exclude counter fields from Breakdown options (#155532) Closes https://github.com/elastic/kibana/issues/155143 ## Summary This PR excludes time series counter fields (like `bytes_counter`) from "Break down by" options in the histogram. --- .../chart/utils/field_supports_breakdown.test.ts | 15 +++++++++++++++ .../chart/utils/field_supports_breakdown.ts | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.test.ts b/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.test.ts index b38b42cf2a249b..5ec2d8a1fc6385 100644 --- a/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.test.ts +++ b/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.test.ts @@ -31,5 +31,20 @@ describe('fieldSupportsBreakdown', () => { expect( fieldSupportsBreakdown({ aggregatable: true, scripted: false, type: 'string' } as any) ).toBe(true); + + expect( + fieldSupportsBreakdown({ aggregatable: true, scripted: false, type: 'number' } as any) + ).toBe(true); + }); + + it('should return false if field is aggregatable but it is a time series counter metric', () => { + expect( + fieldSupportsBreakdown({ + aggregatable: true, + scripted: false, + type: 'number', + timeSeriesMetric: 'counter', + } as any) + ).toBe(false); }); }); diff --git a/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.ts b/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.ts index 302a5950fefcb4..88ec604c1462e6 100644 --- a/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.ts +++ b/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.ts @@ -11,4 +11,7 @@ import { DataViewField } from '@kbn/data-views-plugin/public'; const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']); export const fieldSupportsBreakdown = (field: DataViewField) => - supportedTypes.has(field.type) && field.aggregatable && !field.scripted; + supportedTypes.has(field.type) && + field.aggregatable && + !field.scripted && + field.timeSeriesMetric !== 'counter'; From d6d933a2af12764970a85fa5eb3a62eebf02e41b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Sat, 22 Apr 2023 09:25:37 +0200 Subject: [PATCH 22/65] [ML] Moves shared code to `@kbn/ml-error-utils`. (#155372) - Moves code from `x-pack/plugins/ml/common/util/errors` that was shared via `x-pack/plugins/ml/public/shared.ts` to `@kbn/ml-error-utils`. - `data_visualizer` and `aiops` plugins now use that package instead of code duplication. --- .github/CODEOWNERS | 1 + package.json | 1 + tsconfig.base.json | 2 + x-pack/packages/ml/error_utils/README.md | 3 + .../ml/error_utils}/index.ts | 10 +- x-pack/packages/ml/error_utils/jest.config.js | 12 ++ x-pack/packages/ml/error_utils/kibana.jsonc | 5 + x-pack/packages/ml/error_utils/package.json | 6 + .../error_utils/src/process_errors.test.ts} | 3 +- .../ml/error_utils/src}/process_errors.ts | 22 +- .../ml/error_utils/src}/request_error.ts | 15 ++ x-pack/packages/ml/error_utils/src/types.ts | 196 ++++++++++++++++++ x-pack/packages/ml/error_utils/tsconfig.json | 22 ++ .../public/application/utils/error_utils.ts | 193 ----------------- .../public/hooks/use_document_count_stats.ts | 2 +- x-pack/plugins/aiops/tsconfig.json | 2 +- .../field_data_expanded_row/error_message.tsx | 4 +- .../hooks/use_overall_stats.ts | 2 +- .../requests/get_boolean_field_stats.ts | 2 +- .../requests/get_date_field_stats.ts | 2 +- .../requests/get_field_examples.ts | 2 +- .../requests/get_numeric_field_stats.ts | 2 +- .../requests/get_string_field_stats.ts | 2 +- .../utils/error_utils.ts | 184 ---------------- x-pack/plugins/data_visualizer/tsconfig.json | 2 +- x-pack/plugins/ml/common/constants/search.ts | 5 - x-pack/plugins/ml/common/index.ts | 1 - .../ml/common/types/data_frame_analytics.ts | 2 +- x-pack/plugins/ml/common/types/job_service.ts | 2 +- .../plugins/ml/common/types/job_validation.ts | 2 +- x-pack/plugins/ml/common/types/modules.ts | 2 +- x-pack/plugins/ml/common/types/results.ts | 2 +- .../plugins/ml/common/types/saved_objects.ts | 2 +- x-pack/plugins/ml/common/util/errors/types.ts | 74 ------- .../components/data_grid/common.ts | 6 +- .../import_jobs_flyout/import_jobs_flyout.tsx | 7 +- .../rule_editor/rule_editor_flyout.js | 7 +- .../scatterplot_matrix/scatterplot_matrix.tsx | 10 +- .../data_frame_analytics/common/analytics.ts | 2 +- .../common/get_index_data.ts | 2 +- .../common/use_results_view_config.ts | 3 +- .../create_analytics_advanced_editor.tsx | 4 +- .../details_step/details_step_form.tsx | 2 +- .../components/shared/fetch_explain_data.ts | 3 +- .../validation_step_wrapper.tsx | 6 +- .../hooks/use_index_data.ts | 6 +- .../exploration_query_bar.tsx | 29 ++- .../use_exploration_results.ts | 4 +- .../action_clone/clone_action_name.tsx | 2 +- .../action_delete/use_delete_action.tsx | 2 +- .../use_create_analytics_form.ts | 3 +- .../analytics_service/delete_analytics.ts | 3 +- .../explorer_query_bar/explorer_query_bar.tsx | 15 +- .../application/explorer/explorer_utils.ts | 5 +- .../job_details/job_messages_pane.tsx | 2 +- .../util/model_memory_estimator.ts | 6 +- .../quick_create_job_base.ts | 2 +- .../job_from_lens/visualization_extractor.ts | 2 +- .../components/data_view/change_data_view.tsx | 5 +- .../category_stopped_partitions.tsx | 2 +- .../categorization_view/top_categories.tsx | 2 +- .../post_save_options/post_save_options.tsx | 5 +- .../new_job/recognize/components/job_item.tsx | 2 +- .../recognize/components/kibana_objects.tsx | 2 +- .../test_models/models/inference_base.ts | 2 +- .../inference_input_form/index_input.tsx | 6 +- .../inference_input_form/text_input.tsx | 7 +- .../results_service/result_service_rx.ts | 2 +- .../toast_notification_service.ts | 6 +- .../calendars/list/delete_calendars.js | 5 +- .../forecasting_modal/forecasting_modal.js | 9 +- .../timeseries_chart_with_tooltip.tsx | 2 +- .../get_focus_data.ts | 2 +- .../job_creation/common/job_details.tsx | 13 +- .../layer/incompatible_layer.tsx | 6 +- x-pack/plugins/ml/public/shared.ts | 1 - .../models/data_frame_analytics/validation.ts | 2 +- x-pack/plugins/ml/tsconfig.json | 2 +- .../legacy_uptime/state/api/ml_anomaly.ts | 2 +- x-pack/plugins/synthetics/tsconfig.json | 1 + .../public/app/hooks/use_delete_transform.tsx | 4 +- .../source_search_bar/source_search_bar.tsx | 12 +- .../components/step_define/common/index.ts | 2 +- .../components/step_define/common/types.ts | 5 - .../step_define/hooks/use_search_bar.ts | 12 +- .../server/routes/api/error_utils.ts | 4 +- x-pack/plugins/transform/tsconfig.json | 3 +- .../translations/translations/fr-FR.json | 2 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- yarn.lock | 4 + 91 files changed, 449 insertions(+), 611 deletions(-) create mode 100644 x-pack/packages/ml/error_utils/README.md rename x-pack/{plugins/ml/common/util/errors => packages/ml/error_utils}/index.ts (65%) create mode 100644 x-pack/packages/ml/error_utils/jest.config.js create mode 100644 x-pack/packages/ml/error_utils/kibana.jsonc create mode 100644 x-pack/packages/ml/error_utils/package.json rename x-pack/{plugins/ml/common/util/errors/errors.test.ts => packages/ml/error_utils/src/process_errors.test.ts} (95%) rename x-pack/{plugins/ml/common/util/errors => packages/ml/error_utils/src}/process_errors.ts (86%) rename x-pack/{plugins/ml/common/util/errors => packages/ml/error_utils/src}/request_error.ts (75%) create mode 100644 x-pack/packages/ml/error_utils/src/types.ts create mode 100644 x-pack/packages/ml/error_utils/tsconfig.json delete mode 100644 x-pack/plugins/aiops/public/application/utils/error_utils.ts delete mode 100644 x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts delete mode 100644 x-pack/plugins/ml/common/util/errors/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9b3a1b7a7909bc..63cf2327516fac 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -455,6 +455,7 @@ src/plugins/maps_ems @elastic/kibana-gis x-pack/plugins/maps @elastic/kibana-gis x-pack/packages/ml/agg_utils @elastic/ml-ui x-pack/packages/ml/date_picker @elastic/ml-ui +x-pack/packages/ml/error_utils @elastic/ml-ui x-pack/packages/ml/is_defined @elastic/ml-ui x-pack/packages/ml/is_populated_object @elastic/ml-ui x-pack/packages/ml/local_storage @elastic/ml-ui diff --git a/package.json b/package.json index b7dc6a0b647d62..56fdffa498b050 100644 --- a/package.json +++ b/package.json @@ -470,6 +470,7 @@ "@kbn/maps-plugin": "link:x-pack/plugins/maps", "@kbn/ml-agg-utils": "link:x-pack/packages/ml/agg_utils", "@kbn/ml-date-picker": "link:x-pack/packages/ml/date_picker", + "@kbn/ml-error-utils": "link:x-pack/packages/ml/error_utils", "@kbn/ml-is-defined": "link:x-pack/packages/ml/is_defined", "@kbn/ml-is-populated-object": "link:x-pack/packages/ml/is_populated_object", "@kbn/ml-local-storage": "link:x-pack/packages/ml/local_storage", diff --git a/tsconfig.base.json b/tsconfig.base.json index 60c1f23d037b8c..e96e5439e9c2ca 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -904,6 +904,8 @@ "@kbn/ml-agg-utils/*": ["x-pack/packages/ml/agg_utils/*"], "@kbn/ml-date-picker": ["x-pack/packages/ml/date_picker"], "@kbn/ml-date-picker/*": ["x-pack/packages/ml/date_picker/*"], + "@kbn/ml-error-utils": ["x-pack/packages/ml/error_utils"], + "@kbn/ml-error-utils/*": ["x-pack/packages/ml/error_utils/*"], "@kbn/ml-is-defined": ["x-pack/packages/ml/is_defined"], "@kbn/ml-is-defined/*": ["x-pack/packages/ml/is_defined/*"], "@kbn/ml-is-populated-object": ["x-pack/packages/ml/is_populated_object"], diff --git a/x-pack/packages/ml/error_utils/README.md b/x-pack/packages/ml/error_utils/README.md new file mode 100644 index 00000000000000..d8f2837dffbd34 --- /dev/null +++ b/x-pack/packages/ml/error_utils/README.md @@ -0,0 +1,3 @@ +# @kbn/ml-error-utils + +Empty package generated by @kbn/generate diff --git a/x-pack/plugins/ml/common/util/errors/index.ts b/x-pack/packages/ml/error_utils/index.ts similarity index 65% rename from x-pack/plugins/ml/common/util/errors/index.ts rename to x-pack/packages/ml/error_utils/index.ts index f6566c98490dad..9eac8a4d1c70bd 100644 --- a/x-pack/plugins/ml/common/util/errors/index.ts +++ b/x-pack/packages/ml/error_utils/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -export { MLRequestFailure } from './request_error'; -export { extractErrorMessage, extractErrorProperties } from './process_errors'; +export { MLRequestFailure } from './src/request_error'; +export { extractErrorMessage, extractErrorProperties } from './src/process_errors'; export type { ErrorType, ErrorMessage, @@ -14,6 +14,8 @@ export type { EsErrorRootCause, MLErrorObject, MLHttpFetchError, + MLHttpFetchErrorBase, MLResponseError, -} from './types'; -export { isBoomError, isErrorString, isEsErrorBody, isMLResponseError } from './types'; + QueryErrorMessage, +} from './src/types'; +export { isBoomError, isErrorString, isEsErrorBody, isMLResponseError } from './src/types'; diff --git a/x-pack/packages/ml/error_utils/jest.config.js b/x-pack/packages/ml/error_utils/jest.config.js new file mode 100644 index 00000000000000..f5da4010405755 --- /dev/null +++ b/x-pack/packages/ml/error_utils/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/error_utils'], +}; diff --git a/x-pack/packages/ml/error_utils/kibana.jsonc b/x-pack/packages/ml/error_utils/kibana.jsonc new file mode 100644 index 00000000000000..7629766aca7a7e --- /dev/null +++ b/x-pack/packages/ml/error_utils/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/ml-error-utils", + "owner": "@elastic/ml-ui" +} diff --git a/x-pack/packages/ml/error_utils/package.json b/x-pack/packages/ml/error_utils/package.json new file mode 100644 index 00000000000000..9f0e6c09ef578f --- /dev/null +++ b/x-pack/packages/ml/error_utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/ml-error-utils", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/plugins/ml/common/util/errors/errors.test.ts b/x-pack/packages/ml/error_utils/src/process_errors.test.ts similarity index 95% rename from x-pack/plugins/ml/common/util/errors/errors.test.ts rename to x-pack/packages/ml/error_utils/src/process_errors.test.ts index 5b1e113d06f92e..5624c2f0c7477e 100644 --- a/x-pack/plugins/ml/common/util/errors/errors.test.ts +++ b/x-pack/packages/ml/error_utils/src/process_errors.test.ts @@ -7,7 +7,8 @@ import Boom from '@hapi/boom'; -import { extractErrorMessage, MLHttpFetchError, EsErrorBody } from '.'; +import { extractErrorMessage } from './process_errors'; +import { type MLHttpFetchError, type EsErrorBody } from './types'; describe('ML - error message utils', () => { describe('extractErrorMessage', () => { diff --git a/x-pack/plugins/ml/common/util/errors/process_errors.ts b/x-pack/packages/ml/error_utils/src/process_errors.ts similarity index 86% rename from x-pack/plugins/ml/common/util/errors/process_errors.ts rename to x-pack/packages/ml/error_utils/src/process_errors.ts index 0da2650fa5fd66..1a50fd6ce34948 100644 --- a/x-pack/plugins/ml/common/util/errors/process_errors.ts +++ b/x-pack/packages/ml/error_utils/src/process_errors.ts @@ -16,15 +16,19 @@ import { isMLResponseError, } from './types'; +/** + * Extract properties of the error object from within the response error + * coming from Kibana, Elasticsearch, and our own ML messages. + * + * @param {ErrorType} error + * @returns {MLErrorObject} + */ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { - // extract properties of the error object from within the response error - // coming from Kibana, Elasticsearch, and our own ML messages - // some responses contain raw es errors as part of a bulk response // e.g. if some jobs fail the action in a bulk request if (isEsErrorBody(error)) { return { - message: error.error.reason, + message: error.error.reason ?? '', statusCode: error.status, fullError: error, }; @@ -79,7 +83,7 @@ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { typeof error.body.attributes.body.error.root_cause[0] === 'object' && isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script']) ) { - errObj.causedBy = error.body.attributes.body.error.root_cause[0].script; + errObj.causedBy = error.body.attributes.body.error.root_cause[0].script as string; errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`; } return errObj; @@ -103,8 +107,14 @@ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { }; }; +/** + * Extract only the error message within the response error + * coming from Kibana, Elasticsearch, and our own ML messages. + * + * @param {ErrorType} error + * @returns {string} + */ export const extractErrorMessage = (error: ErrorType): string => { - // extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages const errorObj = extractErrorProperties(error); return errorObj.message; }; diff --git a/x-pack/plugins/ml/common/util/errors/request_error.ts b/x-pack/packages/ml/error_utils/src/request_error.ts similarity index 75% rename from x-pack/plugins/ml/common/util/errors/request_error.ts rename to x-pack/packages/ml/error_utils/src/request_error.ts index 57d63e6cf54b84..a3701298713238 100644 --- a/x-pack/plugins/ml/common/util/errors/request_error.ts +++ b/x-pack/packages/ml/error_utils/src/request_error.ts @@ -7,7 +7,22 @@ import { MLErrorObject, ErrorType } from './types'; +/** + * ML Request Failure + * + * @export + * @class MLRequestFailure + * @typedef {MLRequestFailure} + * @extends {Error} + */ export class MLRequestFailure extends Error { + /** + * Creates an instance of MLRequestFailure. + * + * @constructor + * @param {MLErrorObject} error + * @param {ErrorType} resp + */ constructor(error: MLErrorObject, resp: ErrorType) { super(error.message); Object.setPrototypeOf(this, new.target.prototype); diff --git a/x-pack/packages/ml/error_utils/src/types.ts b/x-pack/packages/ml/error_utils/src/types.ts new file mode 100644 index 00000000000000..b66c960b8c8c0f --- /dev/null +++ b/x-pack/packages/ml/error_utils/src/types.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type Boom from '@hapi/boom'; + +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + +/** + * Short hand type of estypes.ErrorCause. + * @typedef {EsErrorRootCause} + */ +export type EsErrorRootCause = estypes.ErrorCause; + +/** + * Short hand type of estypes.ErrorResponseBase. + * @typedef {EsErrorBody} + */ +export type EsErrorBody = estypes.ErrorResponseBase; + +/** + * ML Response error + * @export + * @interface MLResponseError + * @typedef {MLResponseError} + */ +export interface MLResponseError { + /** + * statusCode + * @type {number} + */ + statusCode: number; + /** + * error + * @type {string} + */ + error: string; + /** + * message + * @type {string} + */ + message: string; + /** + * Optional attributes + * @type {?{ + body: EsErrorBody; + }} + */ + attributes?: { + body: EsErrorBody; + }; +} + +/** + * Interface holding error message + * @export + * @interface ErrorMessage + * @typedef {ErrorMessage} + */ +export interface ErrorMessage { + /** + * message + * @type {string} + */ + message: string; +} + +/** + * To be used for client side errors related to search query bars. + */ +export interface QueryErrorMessage extends ErrorMessage { + /** + * query + * @type {string} + */ + query: string; +} + +/** + * ML Error Object + * @export + * @interface MLErrorObject + * @typedef {MLErrorObject} + */ +export interface MLErrorObject { + /** + * Optional causedBy + * @type {?string} + */ + causedBy?: string; + /** + * message + * @type {string} + */ + message: string; + /** + * Optional statusCode + * @type {?number} + */ + statusCode?: number; + /** + * Optional fullError + * @type {?EsErrorBody} + */ + fullError?: EsErrorBody; +} + +/** + * MLHttpFetchErrorBase + * @export + * @interface MLHttpFetchErrorBase + * @typedef {MLHttpFetchErrorBase} + * @template T + * @extends {IHttpFetchError} + */ +export interface MLHttpFetchErrorBase extends IHttpFetchError { + /** + * body + * @type {T} + */ + body: T; +} + +/** + * MLHttpFetchError + * @export + * @typedef {MLHttpFetchError} + */ +export type MLHttpFetchError = MLHttpFetchErrorBase; + +/** + * Union type of error types + * @export + * @typedef {ErrorType} + */ +export type ErrorType = MLHttpFetchError | EsErrorBody | Boom.Boom | string | undefined; + +/** + * Type guard to check if error is of type EsErrorBody + * @export + * @param {unknown} error + * @returns {error is EsErrorBody} + */ +export function isEsErrorBody(error: unknown): error is EsErrorBody { + return isPopulatedObject(error, ['error']) && isPopulatedObject(error.error, ['reason']); +} + +/** + * Type guard to check if error is a string. + * @export + * @param {unknown} error + * @returns {error is string} + */ +export function isErrorString(error: unknown): error is string { + return typeof error === 'string'; +} + +/** + * Type guard to check if error is of type ErrorMessage. + * @export + * @param {unknown} error + * @returns {error is ErrorMessage} + */ +export function isErrorMessage(error: unknown): error is ErrorMessage { + return isPopulatedObject(error, ['message']) && typeof error.message === 'string'; +} + +/** + * Type guard to check if error is of type MLResponseError. + * @export + * @param {unknown} error + * @returns {error is MLResponseError} + */ +export function isMLResponseError(error: unknown): error is MLResponseError { + return ( + isPopulatedObject(error, ['body']) && + isPopulatedObject(error.body, ['message']) && + 'message' in error.body + ); +} + +/** + * Type guard to check if error is of type Boom. + * @export + * @param {unknown} error + * @returns {error is Boom.Boom} + */ +export function isBoomError(error: unknown): error is Boom.Boom { + return isPopulatedObject(error, ['isBoom']) && error.isBoom === true; +} diff --git a/x-pack/packages/ml/error_utils/tsconfig.json b/x-pack/packages/ml/error_utils/tsconfig.json new file mode 100644 index 00000000000000..de1c550b0e1ab8 --- /dev/null +++ b/x-pack/packages/ml/error_utils/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/ml-is-populated-object", + "@kbn/core-http-browser", + ] +} diff --git a/x-pack/plugins/aiops/public/application/utils/error_utils.ts b/x-pack/plugins/aiops/public/application/utils/error_utils.ts deleted file mode 100644 index f1f1c34dd29596..00000000000000 --- a/x-pack/plugins/aiops/public/application/utils/error_utils.ts +++ /dev/null @@ -1,193 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// TODO Consolidate with duplicate error utils file in -// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts` - -import type { IHttpFetchError } from '@kbn/core-http-browser'; -import Boom from '@hapi/boom'; -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; - -export interface WrappedError { - body: { - attributes: { - body: EsErrorBody; - }; - message: Boom.Boom; - }; - statusCode: number; -} - -export interface EsErrorRootCause { - type: string; - reason: string; - caused_by?: EsErrorRootCause; - script?: string; -} - -export interface EsErrorBody { - error: { - root_cause?: EsErrorRootCause[]; - caused_by?: EsErrorRootCause; - type: string; - reason: string; - }; - status: number; -} - -export interface AiOpsResponseError { - statusCode: number; - error: string; - message: string; - attributes?: { - body: EsErrorBody; - }; -} - -export interface ErrorMessage { - message: string; -} - -export interface AiOpsErrorObject { - causedBy?: string; - message: string; - statusCode?: number; - fullError?: EsErrorBody; -} - -export interface AiOpsHttpFetchError extends IHttpFetchError { - body: T; -} - -export type ErrorType = - | WrappedError - | AiOpsHttpFetchError - | EsErrorBody - | Boom.Boom - | string - | undefined; - -export function isEsErrorBody(error: any): error is EsErrorBody { - return error && error.error?.reason !== undefined; -} - -export function isErrorString(error: any): error is string { - return typeof error === 'string'; -} - -export function isErrorMessage(error: any): error is ErrorMessage { - return error && error.message !== undefined && typeof error.message === 'string'; -} - -export function isAiOpsResponseError(error: any): error is AiOpsResponseError { - return typeof error.body === 'object' && 'message' in error.body; -} - -export function isBoomError(error: any): error is Boom.Boom { - return error?.isBoom === true; -} - -export function isWrappedError(error: any): error is WrappedError { - return error && isBoomError(error.body?.message) === true; -} - -export const extractErrorProperties = (error: ErrorType): AiOpsErrorObject => { - // extract properties of the error object from within the response error - // coming from Kibana, Elasticsearch, and our own AiOps messages - - // some responses contain raw es errors as part of a bulk response - // e.g. if some jobs fail the action in a bulk request - - if (isEsErrorBody(error)) { - return { - message: error.error.reason, - statusCode: error.status, - fullError: error, - }; - } - - if (isErrorString(error)) { - return { - message: error, - }; - } - if (isWrappedError(error)) { - return error.body.message?.output?.payload; - } - - if (isBoomError(error)) { - return { - message: error.output.payload.message, - statusCode: error.output.payload.statusCode, - }; - } - - if (error?.body === undefined && !error?.message) { - return { - message: '', - }; - } - - if (typeof error.body === 'string') { - return { - message: error.body, - }; - } - - if (isAiOpsResponseError(error)) { - if ( - typeof error.body.attributes === 'object' && - typeof error.body.attributes.body?.error?.reason === 'string' - ) { - const errObj: AiOpsErrorObject = { - message: error.body.attributes.body.error.reason, - statusCode: error.body.statusCode, - fullError: error.body.attributes.body, - }; - if ( - typeof error.body.attributes.body.error.caused_by === 'object' && - (typeof error.body.attributes.body.error.caused_by?.reason === 'string' || - typeof error.body.attributes.body.error.caused_by?.caused_by?.reason === 'string') - ) { - errObj.causedBy = - error.body.attributes.body.error.caused_by?.caused_by?.reason || - error.body.attributes.body.error.caused_by?.reason; - } - if ( - Array.isArray(error.body.attributes.body.error.root_cause) && - typeof error.body.attributes.body.error.root_cause[0] === 'object' && - isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script']) - ) { - errObj.causedBy = error.body.attributes.body.error.root_cause[0].script; - errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`; - } - return errObj; - } else { - return { - message: error.body.message, - statusCode: error.body.statusCode, - }; - } - } - - if (isErrorMessage(error)) { - return { - message: error.message, - }; - } - - // If all else fail return an empty message instead of JSON.stringify - return { - message: '', - }; -}; - -export const extractErrorMessage = (error: ErrorType): string => { - // extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages - const errorObj = extractErrorProperties(error); - return errorObj.message; -}; diff --git a/x-pack/plugins/aiops/public/hooks/use_document_count_stats.ts b/x-pack/plugins/aiops/public/hooks/use_document_count_stats.ts index 9e4f50368a0b5c..8cfaa074286d63 100644 --- a/x-pack/plugins/aiops/public/hooks/use_document_count_stats.ts +++ b/x-pack/plugins/aiops/public/hooks/use_document_count_stats.ts @@ -12,10 +12,10 @@ import { i18n } from '@kbn/i18n'; import type { ToastsStart } from '@kbn/core/public'; import { stringHash } from '@kbn/ml-string-hash'; import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { RANDOM_SAMPLER_SEED } from '../../common/constants'; -import { extractErrorProperties } from '../application/utils/error_utils'; import { DocumentCountStats, getDocumentCountStatsRequest, diff --git a/x-pack/plugins/aiops/tsconfig.json b/x-pack/plugins/aiops/tsconfig.json index b9a6ff5408eda9..89c236ba25c992 100644 --- a/x-pack/plugins/aiops/tsconfig.json +++ b/x-pack/plugins/aiops/tsconfig.json @@ -34,7 +34,6 @@ "@kbn/ui-theme", "@kbn/i18n-react", "@kbn/rison", - "@kbn/core-http-browser", "@kbn/aiops-components", "@kbn/aiops-utils", "@kbn/licensing-plugin", @@ -51,6 +50,7 @@ "@kbn/ml-route-utils", "@kbn/unified-field-list-plugin", "@kbn/ml-random-sampler-utils", + "@kbn/ml-error-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx index 54cbeb28cf6f31..0f3dc78f97487b 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/error_message.tsx @@ -8,14 +8,14 @@ import { EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; -import { DVErrorObject } from '../../../../../index_data_visualizer/utils/error_utils'; +import { MLErrorObject } from '@kbn/ml-error-utils'; export const ErrorMessageContent = ({ fieldName, error, }: { fieldName: string; - error: DVErrorObject; + error: MLErrorObject; }) => { return ( diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts index ced7bf10587628..3419b1674fddcb 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts @@ -16,6 +16,7 @@ import type { IKibanaSearchResponse, ISearchOptions, } from '@kbn/data-plugin/common'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { useDataVisualizerKibana } from '../../kibana_context'; import { AggregatableFieldOverallStats, @@ -29,7 +30,6 @@ import { } from '../search_strategy/requests/overall_stats'; import type { OverallStats } from '../types/overall_stats'; import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view'; -import { extractErrorProperties } from '../utils/error_utils'; import { DataStatsFetchProgress, isRandomSamplingOption, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts index bf941e09526574..e394f6456d8f52 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts @@ -15,6 +15,7 @@ import type { ISearchStart, } from '@kbn/data-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { processTopValues } from './utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; @@ -25,7 +26,6 @@ import type { FieldStatsCommonRequestParams, } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; -import { extractErrorProperties } from '../../utils/error_utils'; export const getBooleanFieldsStatsRequest = ( params: FieldStatsCommonRequestParams, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts index 863cd6885fe885..4bd914f3637e54 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts @@ -16,11 +16,11 @@ import type { ISearchStart, } from '@kbn/data-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; import type { FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; import type { Field, DateFieldStats, Aggs } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; -import { extractErrorProperties } from '../../utils/error_utils'; export const getDateFieldsStatsRequest = ( params: FieldStatsCommonRequestParams, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts index df7afb16479f09..3943979d290d9f 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts @@ -17,6 +17,7 @@ import type { import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { buildBaseFilterCriteria } from '@kbn/ml-query-utils'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { getUniqGeoOrStrExamples } from '../../../common/util/example_utils'; import type { Field, @@ -24,7 +25,6 @@ import type { FieldStatsCommonRequestParams, } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; -import { extractErrorProperties } from '../../utils/error_utils'; import { MAX_EXAMPLES_DEFAULT } from './constants'; export const getFieldExamplesRequest = (params: FieldStatsCommonRequestParams, field: Field) => { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts index b9dd55351781f3..f7d1b39f15d3fe 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts @@ -18,6 +18,7 @@ import type { import type { ISearchStart } from '@kbn/data-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { isDefined } from '@kbn/ml-is-defined'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { processTopValues } from './utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; import { MAX_PERCENT, PERCENTILE_SPACING, SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; @@ -32,7 +33,6 @@ import type { FieldStatsError, } from '../../../../../common/types/field_stats'; import { processDistributionData } from '../../utils/process_distribution_data'; -import { extractErrorProperties } from '../../utils/error_utils'; import { isIKibanaSearchResponse, isNormalSamplingOption, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts index a035842fa87675..159be48b338e42 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts @@ -16,6 +16,7 @@ import type { ISearchStart, } from '@kbn/data-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { processTopValues } from './utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; import { SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; @@ -26,7 +27,6 @@ import type { StringFieldStats, } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; -import { extractErrorProperties } from '../../utils/error_utils'; export const getStringFieldStatsRequest = ( params: FieldStatsCommonRequestParams, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts deleted file mode 100644 index 06a9b0b4002a6a..00000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts +++ /dev/null @@ -1,184 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IHttpFetchError } from '@kbn/core-http-browser'; -import Boom from '@hapi/boom'; -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; - -export interface WrappedError { - body: { - attributes: { - body: EsErrorBody; - }; - message: Boom.Boom; - }; - statusCode: number; -} - -export interface EsErrorRootCause { - type: string; - reason: string; - caused_by?: EsErrorRootCause; - script?: string; -} - -export interface EsErrorBody { - error: { - root_cause?: EsErrorRootCause[]; - caused_by?: EsErrorRootCause; - type: string; - reason: string; - }; - status: number; -} - -export interface DVResponseError { - statusCode: number; - error: string; - message: string; - attributes?: { - body: EsErrorBody; - }; -} - -export interface ErrorMessage { - message: string; -} - -export interface DVErrorObject { - causedBy?: string; - message: string; - statusCode?: number; - fullError?: EsErrorBody; -} - -export interface DVHttpFetchError extends IHttpFetchError { - body: T; -} - -export type ErrorType = - | WrappedError - | DVHttpFetchError - | EsErrorBody - | Boom.Boom - | string - | undefined; - -export function isEsErrorBody(error: any): error is EsErrorBody { - return error && error.error?.reason !== undefined; -} - -export function isErrorString(error: any): error is string { - return typeof error === 'string'; -} - -export function isErrorMessage(error: any): error is ErrorMessage { - return error && error.message !== undefined && typeof error.message === 'string'; -} - -export function isDVResponseError(error: any): error is DVResponseError { - return typeof error.body === 'object' && 'message' in error.body; -} - -export function isBoomError(error: any): error is Boom.Boom { - return error?.isBoom === true; -} - -export function isWrappedError(error: any): error is WrappedError { - return error && isBoomError(error.body?.message) === true; -} - -export const extractErrorProperties = (error: ErrorType): DVErrorObject => { - // extract properties of the error object from within the response error - // coming from Kibana, Elasticsearch, and our own DV messages - - // some responses contain raw es errors as part of a bulk response - // e.g. if some jobs fail the action in a bulk request - - if (isEsErrorBody(error)) { - return { - message: error.error.reason, - statusCode: error.status, - fullError: error, - }; - } - - if (isErrorString(error)) { - return { - message: error, - }; - } - if (isWrappedError(error)) { - return error.body.message?.output?.payload; - } - - if (isBoomError(error)) { - return { - message: error.output.payload.message, - statusCode: error.output.payload.statusCode, - }; - } - - if (error?.body === undefined && !error?.message) { - return { - message: '', - }; - } - - if (typeof error.body === 'string') { - return { - message: error.body, - }; - } - - if (isDVResponseError(error)) { - if ( - typeof error.body.attributes === 'object' && - typeof error.body.attributes.body?.error?.reason === 'string' - ) { - const errObj: DVErrorObject = { - message: error.body.attributes.body.error.reason, - statusCode: error.body.statusCode, - fullError: error.body.attributes.body, - }; - if ( - typeof error.body.attributes.body.error.caused_by === 'object' && - (typeof error.body.attributes.body.error.caused_by?.reason === 'string' || - typeof error.body.attributes.body.error.caused_by?.caused_by?.reason === 'string') - ) { - errObj.causedBy = - error.body.attributes.body.error.caused_by?.caused_by?.reason || - error.body.attributes.body.error.caused_by?.reason; - } - if ( - Array.isArray(error.body.attributes.body.error.root_cause) && - typeof error.body.attributes.body.error.root_cause[0] === 'object' && - isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script']) - ) { - errObj.causedBy = error.body.attributes.body.error.root_cause[0].script; - errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`; - } - return errObj; - } else { - return { - message: error.body.message, - statusCode: error.body.statusCode, - }; - } - } - - if (isErrorMessage(error)) { - return { - message: error.message, - }; - } - - // If all else fail return an empty message instead of JSON.stringify - return { - message: '', - }; -}; diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 7f38e7b9b77cb3..4609d1e9497d26 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -17,7 +17,6 @@ "@kbn/cloud-chat-plugin", "@kbn/cloud-plugin", "@kbn/core-execution-context-common", - "@kbn/core-http-browser", "@kbn/core", "@kbn/custom-integrations-plugin", "@kbn/data-plugin", @@ -60,6 +59,7 @@ "@kbn/unified-search-plugin", "@kbn/usage-collection-plugin", "@kbn/utility-types", + "@kbn/ml-error-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/ml/common/constants/search.ts b/x-pack/plugins/ml/common/constants/search.ts index 9985b502b085cd..8ff9b022c274f6 100644 --- a/x-pack/plugins/ml/common/constants/search.ts +++ b/x-pack/plugins/ml/common/constants/search.ts @@ -14,8 +14,3 @@ export const SEARCH_QUERY_LANGUAGE = { } as const; export type SearchQueryLanguage = typeof SEARCH_QUERY_LANGUAGE[keyof typeof SEARCH_QUERY_LANGUAGE]; - -export interface ErrorMessage { - query: string; - message: string; -} diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index 8d419f120a5643..b540c4d3751f13 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -16,7 +16,6 @@ export { export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; export { composeValidators, patternValidator } from './util/validators'; export { isRuntimeMappings, isRuntimeField } from './util/runtime_field_utils'; -export { extractErrorMessage } from './util/errors'; export type { RuntimeMappings } from './types/fields'; export { getDefaultCapabilities as getDefaultMlCapabilities } from './types/capabilities'; export { DATAFEED_STATE, JOB_STATE } from './constants/states'; diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 26bdd29ac3090b..cba66124bab4b7 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import { EsErrorBody } from '../util/errors'; +import { EsErrorBody } from '@kbn/ml-error-utils'; import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; import type { UrlConfig } from './custom_urls'; diff --git a/x-pack/plugins/ml/common/types/job_service.ts b/x-pack/plugins/ml/common/types/job_service.ts index a3e1571070ffdd..cf029111e0577e 100644 --- a/x-pack/plugins/ml/common/types/job_service.ts +++ b/x-pack/plugins/ml/common/types/job_service.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { ErrorType } from '@kbn/ml-error-utils'; import { Job, JobStats, IndicesOptions } from './anomaly_detection_jobs'; import { RuntimeMappings } from './fields'; import { ES_AGGREGATION } from '../constants/aggregation_types'; -import { ErrorType } from '../util/errors'; export interface MlJobsResponse { jobs: Job[]; diff --git a/x-pack/plugins/ml/common/types/job_validation.ts b/x-pack/plugins/ml/common/types/job_validation.ts index 0c1db63ff37625..226166d45c956f 100644 --- a/x-pack/plugins/ml/common/types/job_validation.ts +++ b/x-pack/plugins/ml/common/types/job_validation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ErrorType } from '../util/errors'; +import type { ErrorType } from '@kbn/ml-error-utils'; export interface DatafeedValidationResponse { valid: boolean; diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts index dd9f098cabe1c7..2a6cc9bbd57ce2 100644 --- a/x-pack/plugins/ml/common/types/modules.ts +++ b/x-pack/plugins/ml/common/types/modules.ts @@ -6,8 +6,8 @@ */ import type { SavedObjectAttributes } from '@kbn/core/types'; +import type { ErrorType } from '@kbn/ml-error-utils'; import type { Datafeed, Job } from './anomaly_detection_jobs'; -import type { ErrorType } from '../util/errors'; export interface ModuleJob { id: string; diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts index cf25fc6081e16d..3fda97b5740b13 100644 --- a/x-pack/plugins/ml/common/types/results.ts +++ b/x-pack/plugins/ml/common/types/results.ts @@ -7,7 +7,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts'; -import type { ErrorType } from '../util/errors'; +import type { ErrorType } from '@kbn/ml-error-utils'; import type { EntityField } from '../util/anomaly_utils'; import type { Datafeed, JobId, ModelSnapshot } from './anomaly_detection_jobs'; import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../constants/aggregation_types'; diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index ab3b97d1e614d3..adaf00fd9405fb 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ErrorType } from '../util/errors'; +import type { ErrorType } from '@kbn/ml-error-utils'; export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export type TrainedModelType = 'trained-model'; diff --git a/x-pack/plugins/ml/common/util/errors/types.ts b/x-pack/plugins/ml/common/util/errors/types.ts deleted file mode 100644 index 9f4b123e3e45d0..00000000000000 --- a/x-pack/plugins/ml/common/util/errors/types.ts +++ /dev/null @@ -1,74 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IHttpFetchError } from '@kbn/core-http-browser'; -import Boom from '@hapi/boom'; - -export interface EsErrorRootCause { - type: string; - reason: string; - caused_by?: EsErrorRootCause; - script?: string; -} - -export interface EsErrorBody { - error: { - root_cause?: EsErrorRootCause[]; - caused_by?: EsErrorRootCause; - type: string; - reason: string; - }; - status: number; -} - -export interface MLResponseError { - statusCode: number; - error: string; - message: string; - attributes?: { - body: EsErrorBody; - }; -} - -export interface ErrorMessage { - message: string; -} - -export interface MLErrorObject { - causedBy?: string; - message: string; - statusCode?: number; - fullError?: EsErrorBody; -} - -export interface MLHttpFetchErrorBase extends IHttpFetchError { - body: T; -} - -export type MLHttpFetchError = MLHttpFetchErrorBase; - -export type ErrorType = MLHttpFetchError | EsErrorBody | Boom.Boom | string | undefined; - -export function isEsErrorBody(error: any): error is EsErrorBody { - return error && error.error?.reason !== undefined; -} - -export function isErrorString(error: any): error is string { - return typeof error === 'string'; -} - -export function isErrorMessage(error: any): error is ErrorMessage { - return error && error.message !== undefined && typeof error.message === 'string'; -} - -export function isMLResponseError(error: any): error is MLResponseError { - return typeof error.body === 'object' && 'message' in error.body; -} - -export function isBoomError(error: any): error is Boom.Boom { - return error?.isBoom === true; -} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 86587112f37d7e..a32aafe2b06220 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -12,16 +12,14 @@ import { useMemo } from 'react'; import { EuiDataGridCellValueElementProps, EuiDataGridStyle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { CoreSetup } from '@kbn/core/public'; - import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { getNestedProperty } from '@kbn/ml-nested-property'; - import { isCounterTimeSeriesMetric } from '@kbn/ml-agg-utils'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; -import { extractErrorMessage } from '../../../../common/util/errors'; import { FeatureImportance, FeatureImportanceClassName, diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index 0e786a63b222de..94effae6da28d0 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -6,9 +6,7 @@ */ import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import useDebounce from 'react-use/lib/useDebounce'; -import { i18n } from '@kbn/i18n'; import { EuiFlyout, @@ -29,6 +27,10 @@ import { EuiFieldText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { type ErrorType, extractErrorProperties } from '@kbn/ml-error-utils'; + import type { DataFrameAnalyticsConfig } from '../../../data_frame_analytics/common'; import type { JobType } from '../../../../../common/types/saved_objects'; import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; @@ -38,7 +40,6 @@ import { toastNotificationServiceProvider } from '../../../services/toast_notifi import { JobImportService } from './jobs_import_service'; import { useValidateIds } from './validate'; import type { ImportedAdJob, JobIdObject, SkippedJobs } from './jobs_import_service'; -import { ErrorType, extractErrorProperties } from '../../../../../common/util/errors'; interface Props { isDisabled: boolean; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index ed5be09e03b420..668907c010108e 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -11,8 +11,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, @@ -31,6 +29,10 @@ import { EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { DetectorDescriptionList } from './components/detector_description_list'; import { ActionsSection } from './actions_section'; import { checkPermission } from '../../capabilities/check_capabilities'; @@ -54,7 +56,6 @@ import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; -import { extractErrorMessage } from '../../../../common/util/errors'; class RuleEditorFlyoutUI extends Component { static propTypes = { diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index 9a10dc2b782c94..404c8978221254 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -6,9 +6,8 @@ */ import React, { useMemo, useEffect, useState, FC, useCallback } from 'react'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import rison from '@kbn/rison'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiCallOut, EuiComboBox, @@ -23,16 +22,17 @@ import { EuiSwitch, } from '@elastic/eui'; +import rison from '@kbn/rison'; import { i18n } from '@kbn/i18n'; import { Query } from '@kbn/data-plugin/common/query'; - import { DataView } from '@kbn/data-views-plugin/public'; import { stringHash } from '@kbn/ml-string-hash'; -import { extractErrorMessage } from '../../../../common'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; import { RuntimeMappings } from '../../../../common/types/fields'; -import { getCombinedRuntimeMappings } from '../data_grid'; +import { getCombinedRuntimeMappings } from '../data_grid'; import { useMlApiContext, useMlKibana } from '../../contexts/kibana'; import { getProcessedFields } from '../data_grid'; 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 4638d3824ad61e..a1609c601cb72b 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 @@ -9,9 +9,9 @@ import { useEffect } from 'react'; import { BehaviorSubject, Subscription } from 'rxjs'; import { distinctUntilChanged, filter } from 'rxjs/operators'; import { cloneDeep } from 'lodash'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { ml } from '../../services/ml_api_service'; import { Dictionary } from '../../../../common/types/common'; -import { extractErrorMessage } from '../../../../common/util/errors'; import { ClassificationEvaluateResponse, EvaluateMetrics, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts index 29d76d32d88553..afd580da21ea3d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { extractErrorMessage } from '../../../../common/util/errors'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { EsSorting, UseDataGridReturnType, getProcessedFields } from '../../components/data_grid'; import { ml } from '../../services/ml_api_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts index df430142a31933..1908ff9ad614c3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -10,8 +10,7 @@ import { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; - -import { extractErrorMessage } from '../../../../common/util/errors'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { getDataViewIdFromName } from '../../util/index_utils'; import { ml } from '../../services/ml_api_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index ef43f1ee0d08b0..c809597ed12089 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -10,11 +10,11 @@ import { debounce } from 'lodash'; import { EuiCallOut, EuiFieldText, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { CodeEditor } from '@kbn/kibana-react-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { useNotifications } from '../../../../../contexts/kibana'; import { ml } from '../../../../../services/ml_api_service'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { CreateStep } from '../create_step'; import { ANALYTICS_STEPS } from '../../page'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 265018ec23f9f4..dcf5adfba5ff6d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -9,6 +9,7 @@ import React, { FC, Fragment, useRef, useEffect, useMemo, useState } from 'react import { debounce } from 'lodash'; import { EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiSwitch, EuiTextArea } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { useMlKibana } from '../../../../../contexts/kibana'; import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; @@ -16,7 +17,6 @@ import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validat import { ContinueButton } from '../continue_button'; import { ANALYTICS_STEPS } from '../../page'; import { ml } from '../../../../../services/ml_api_service'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; const DEFAULT_RESULTS_FIELD = 'ml'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts index ebf5d2dce27065..e0be92ebe1a265 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { extractErrorProperties } from '@kbn/ml-error-utils'; + import { ml } from '../../../../../services/ml_api_service'; -import { extractErrorProperties } from '../../../../../../../common/util/errors'; import { DfAnalyticsExplainResponse, FieldSelectionItem, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step_wrapper.tsx index d5b2ba8251ad4a..c1256529f357fe 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step_wrapper.tsx @@ -6,9 +6,12 @@ */ import React, { FC, useEffect, useMemo, useState } from 'react'; +import { debounce } from 'lodash'; + import { EuiForm } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; -import { debounce } from 'lodash'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { ValidationStep } from './validation_step'; @@ -16,7 +19,6 @@ import { ValidationStepDetails } from './validation_step_details'; import { ANALYTICS_STEPS } from '../../page'; import { useMlApiContext } from '../../../../../contexts/kibana'; import { getJobConfigFromFormState } from '../../../analytics_management/hooks/use_create_analytics_form/state'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { CalloutMessage, ValidateAnalyticsJobResponse, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index 4f681f07ff69e2..fdd338be65bb26 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -9,11 +9,13 @@ import { useEffect, useMemo, useState } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiDataGridColumn } from '@elastic/eui'; -import { CoreSetup } from '@kbn/core/public'; +import { CoreSetup } from '@kbn/core/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { isRuntimeMappings } from '../../../../../../common/util/runtime_field_utils'; import { RuntimeMappings } from '../../../../../../common/types/fields'; import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../../common/constants/field_histograms'; @@ -34,7 +36,7 @@ import { getProcessedFields, getCombinedRuntimeMappings, } from '../../../../components/data_grid'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; + import { INDEX_STATUS } from '../../../common/analytics'; import { ml } from '../../../../services/ml_api_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index 464d57ff2917af..1ac8404318385e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -6,17 +6,19 @@ */ import React, { FC, useEffect, useMemo, useState } from 'react'; +import { debounce } from 'lodash'; + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiButtonGroup, EuiCode, EuiFlexGroup, EuiFlexItem, EuiInputPopover } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { debounce } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { DataView } from '@kbn/data-views-plugin/common'; import type { Query } from '@kbn/es-query'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; -import { Dictionary } from '../../../../../../../common/types/common'; +import { QueryErrorMessage } from '@kbn/ml-error-utils'; +import { Dictionary } from '../../../../../../../common/types/common'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage, @@ -25,11 +27,6 @@ import { removeFilterFromQueryString } from '../../../../../explorer/explorer_ut import { SavedSearchQuery } from '../../../../../contexts/ml'; import { useMlKibana } from '../../../../../contexts/kibana'; -interface ErrorMessage { - query: string; - message: string; -} - export interface ExplorationQueryBarProps { indexPattern: DataView; setSearchQuery: (update: { @@ -55,7 +52,9 @@ export const ExplorationQueryBar: FC = ({ // The internal state of the input query bar updated on every key stroke. const [searchInput, setSearchInput] = useState(query); const [idToSelectedMap, setIdToSelectedMap] = useState<{ [id: string]: boolean }>({}); - const [errorMessage, setErrorMessage] = useState(undefined); + const [queryErrorMessage, setQueryErrorMessage] = useState( + undefined + ); const { services } = useMlKibana(); const { @@ -119,7 +118,7 @@ export const ExplorationQueryBar: FC = ({ convertedQuery = luceneStringToDsl(query.query as string); break; default: - setErrorMessage({ + setQueryErrorMessage({ query: query.query as string, message: i18n.translate('xpack.ml.queryBar.queryLanguageNotSupported', { defaultMessage: 'Query language is not supported', @@ -133,7 +132,7 @@ export const ExplorationQueryBar: FC = ({ language: query.language, }); } catch (e) { - setErrorMessage({ query: query.query as string, message: e.message }); + setQueryErrorMessage({ query: query.query as string, message: e.message }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [query.query]); @@ -187,7 +186,7 @@ export const ExplorationQueryBar: FC = ({ return ( setErrorMessage(undefined)} + closePopover={() => setQueryErrorMessage(undefined)} input={ @@ -249,14 +248,14 @@ export const ExplorationQueryBar: FC = ({ )} } - isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + isOpen={queryErrorMessage?.query === searchInput.query && queryErrorMessage?.message !== ''} > {i18n.translate('xpack.ml.stepDefineForm.invalidQuery', { defaultMessage: 'Invalid Query', })} {': '} - {errorMessage?.message.split('\n')[0]} + {queryErrorMessage?.message.split('\n')[0]} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index b52c06905792cc..30749558a23a1b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -10,9 +10,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; import { CoreSetup } from '@kbn/core/public'; - import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { MlApiServices } from '../../../../../services/ml_api_service'; import { DataLoader } from '../../../../../datavisualizer/index_based/data_loader'; @@ -35,7 +36,6 @@ import { FEATURE_IMPORTANCE, TOP_CLASSES } from '../../../../common/constants'; import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics'; import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; import { isRegressionAnalysis } from '../../../../common/analytics'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { useTrainedModelsApiService } from '../../../../../services/ml_api_service/trained_models'; import { FeatureImportanceBaseline } from '../../../../../../../common/types/feature_importance'; import { useExplorationDataGrid } from './use_exploration_data_grid'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index 76b85e2384dfd0..0ba51a7ca64dab 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -11,6 +11,7 @@ import { cloneDeep, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; @@ -19,7 +20,6 @@ import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES } from '../../hooks/use_create_analytics_form'; import { State } from '../../hooks/use_create_analytics_form/state'; import { DataFrameAnalyticsListRow } from '../analytics_list/common'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; interface PropDefinition { /** diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx index 4d1565c1769f37..01b6e3a3f50ad2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { useMlKibana } from '../../../../../contexts/kibana'; import { useToastNotificationService } from '../../../../../services/toast_notification_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index cddc4fcd092dcd..4a6ed2176be253 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -10,7 +10,8 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; import { DuplicateDataViewError } from '@kbn/data-plugin/public'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index 537b2016d9af3c..c11490a660f100 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -6,7 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { ml } from '../../../../../services/ml_api_service'; import { ToastNotificationService } from '../../../../../services/toast_notification_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx index bbb18697dab1c7..8f4cd30a4c115e 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx @@ -12,7 +12,8 @@ import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@k import type { Query } from '@kbn/es-query'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { SEARCH_QUERY_LANGUAGE, ErrorMessage } from '../../../../../common/constants/search'; +import type { QueryErrorMessage } from '@kbn/ml-error-utils'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; import { InfluencersFilterQuery } from '../../../../../common/types/es_client'; import { useAnomalyExplorerContext } from '../../anomaly_explorer_context'; import { useMlKibana } from '../../../contexts/kibana'; @@ -129,7 +130,9 @@ export const ExplorerQueryBar: FC = ({ const [searchInput, setSearchInput] = useState( getInitSearchInputState({ filterActive, queryString }) ); - const [errorMessage, setErrorMessage] = useState(undefined); + const [queryErrorMessage, setQueryErrorMessage] = useState( + undefined + ); useEffect( function updateSearchInputFromFilter() { @@ -160,14 +163,14 @@ export const ExplorerQueryBar: FC = ({ } } catch (e) { console.log('Invalid query syntax in search bar', e); // eslint-disable-line no-console - setErrorMessage({ query: query.query as string, message: e.message }); + setQueryErrorMessage({ query: query.query as string, message: e.message }); } }; return ( = ({ }} /> } - isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + isOpen={queryErrorMessage?.query === searchInput.query && queryErrorMessage?.message !== ''} > {i18n.translate('xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar', { defaultMessage: 'Invalid query', })} {': '} - {errorMessage?.message.split('\n')[0]} + {queryErrorMessage?.message.split('\n')[0]} ); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index e7ad6802c44017..f0f26a39184386 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -11,19 +11,20 @@ import { get, union, uniq } from 'lodash'; import moment from 'moment-timezone'; +import { lastValueFrom } from 'rxjs'; + import { ES_FIELD_TYPES } from '@kbn/field-types'; import { asyncForEach } from '@kbn/std'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; -import { lastValueFrom } from 'rxjs'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, } from '../../../common/constants/search'; import { EntityField, getEntityFieldList } from '../../../common/util/anomaly_utils'; import { getDataViewIdFromName } from '../util/index_utils'; -import { extractErrorMessage } from '../../../common/util/errors'; import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types'; import { isSourceDataChartableForDetector, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index 8b3ed99709cb54..86cd23b46383e2 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -9,10 +9,10 @@ import React, { FC, useCallback, useEffect, useState } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; import { JobMessage } from '../../../../../../common/types/audit_message'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; import { useToastNotificationService } from '../../../../services/toast_notification_service'; import { useMlApiContext } from '../../../../contexts/kibana'; import { checkPermission } from '../../../../capabilities/check_capabilities'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts index 159f1af10e69f4..52d4d771062173 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs'; import { isEqual, cloneDeep } from 'lodash'; import { @@ -21,10 +20,13 @@ import { skipWhile, } from 'rxjs/operators'; import { useEffect, useMemo } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { type MLHttpFetchError, extractErrorMessage } from '@kbn/ml-error-utils'; + import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../../../../../common/constants/new_job'; import { ml } from '../../../../../services/ml_api_service'; import { JobValidator, VALIDATION_DELAY_MS } from '../../job_validator/job_validator'; -import { MLHttpFetchError, extractErrorMessage } from '../../../../../../../common/util/errors'; import { useMlKibana } from '../../../../../contexts/kibana'; import { JobCreator } from '../job_creator'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts index a22b6d9fd57a55..f01898fa905a1c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts @@ -17,9 +17,9 @@ import type { Filter, Query, DataViewBase } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { Embeddable } from '@kbn/lens-plugin/public'; import type { MapEmbeddable } from '@kbn/maps-plugin/public'; +import type { ErrorType } from '@kbn/ml-error-utils'; import type { MlApiServices } from '../../../services/ml_api_service'; import { getFiltersForDSLQuery } from '../../../../../common/util/job_utils'; -import type { ErrorType } from '../../../../../common/util/errors'; import { CREATED_BY_LABEL } from '../../../../../common/constants/new_job'; import { createQueries } from '../utils/new_job_utils'; import { createDatafeedId } from '../../../../../common/util/job_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/visualization_extractor.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/visualization_extractor.ts index c7f5a02e75d5b0..4552123184f5c9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/visualization_extractor.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/visualization_extractor.ts @@ -10,8 +10,8 @@ import { layerTypes } from '@kbn/lens-plugin/public'; import { i18n } from '@kbn/i18n'; +import type { ErrorType } from '@kbn/ml-error-utils'; import { JOB_TYPE } from '../../../../../common/constants/new_job'; -import { ErrorType } from '../../../../../common/util/errors'; import { getVisTypeFactory, isCompatibleLayer, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx index 71d68b895f605e..9e68d9f6f8c37a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx @@ -6,7 +6,6 @@ */ import React, { FC, useState, useEffect, useCallback, useContext } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { @@ -23,7 +22,10 @@ import { EuiModalBody, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { JobCreatorContext } from '../../../job_creator_context'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; import { resetAdvancedJob } from '../../../../../common/job_creator/util/general'; @@ -31,7 +33,6 @@ import { CombinedJob, Datafeed, } from '../../../../../../../../../common/types/anomaly_detection_jobs'; -import { extractErrorMessage } from '../../../../../../../../../common/util/errors'; import type { DatafeedValidationResponse } from '../../../../../../../../../common/types/job_validation'; import { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx index 8030292bd9e597..090fcf40d3f17e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx @@ -11,10 +11,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { from } from 'rxjs'; import { switchMap, takeWhile, tap } from 'rxjs/operators'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { JobCreatorContext } from '../../../job_creator_context'; import { CategorizationJobCreator } from '../../../../../common/job_creator'; import { ml } from '../../../../../../../services/ml_api_service'; -import { extractErrorProperties } from '../../../../../../../../../common/util/errors'; const NUMBER_OF_PREVIEW = 5; export const CategoryStoppedPartitions: FC = () => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx index 4491dfab1abdd2..b12b4261a0628b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx @@ -8,13 +8,13 @@ import React, { FC, useContext, useEffect, useState } from 'react'; import { EuiBasicTable, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { JobCreatorContext } from '../../../job_creator_context'; import { CategorizationJobCreator } from '../../../../../common/job_creator'; import { Results } from '../../../../../common/results_loader'; import { ml } from '../../../../../../../services/ml_api_service'; import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; import { NUMBER_OF_CATEGORY_EXAMPLES } from '../../../../../../../../../common/constants/categorization_job'; -import { extractErrorProperties } from '../../../../../../../../../common/util/errors'; export const TopCategories: FC = () => { const { displayErrorToast } = useToastNotificationService(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx index 7fb483b54cf5df..75bc8772e0ac6a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -6,12 +6,15 @@ */ import React, { FC, Fragment, useContext, useState } from 'react'; + import { EuiButton, EuiFlexItem } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { JobRunner } from '../../../../../common/job_runner'; import { useMlKibana } from '../../../../../../../contexts/kibana'; -import { extractErrorMessage } from '../../../../../../../../../common/util/errors'; import { JobCreatorContext } from '../../../job_creator_context'; import { DATAFEED_STATE } from '../../../../../../../../../common/constants/states'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx index 93b60863983589..cd6bd11096c652 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx @@ -18,11 +18,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { ModuleJobUI } from '../page'; import { SETUP_RESULTS_WIDTH } from './module_jobs'; import { tabColor } from '../../../../../../common/util/group_color_utils'; import { JobOverride, DatafeedResponse } from '../../../../../../common/types/modules'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; interface JobItemProps { job: ModuleJobUI; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx index 079e698c3a5c0e..95ac0b4043f579 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx @@ -18,8 +18,8 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { KibanaObjectUi } from '../page'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; export interface KibanaObjectItemProps { objectType: string; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts index 59fa138d0f29f6..1e3e3ecd5dfd77 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { map } from 'rxjs/operators'; import { SupportedPytorchTasksType } from '@kbn/ml-trained-models-utils'; -import { MLHttpFetchError } from '../../../../../common/util/errors'; +import type { MLHttpFetchError } from '@kbn/ml-error-utils'; import { trainedModelsApiProvider } from '../../../services/ml_api_service/trained_models'; import { getInferenceInfoComponent } from './inference_info'; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx index 5060f0033fd6cb..4dbe900283657e 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useMemo, useCallback, FormEventHandler } from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { FormattedMessage } from '@kbn/i18n-react'; + import { EuiSpacer, EuiButton, @@ -21,8 +21,10 @@ import { EuiForm, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { ErrorMessage } from '../../inference_error'; -import { extractErrorMessage } from '../../../../../../common'; import type { InferrerType } from '..'; import { useIndexInput, InferenceInputFormIndexControls } from '../index_input'; import { RUNNING_STATE } from '../inference_base'; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx index 3446bda477b4ae..fc162a305c32b5 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx @@ -6,13 +6,14 @@ */ import React, { FC, useState, useMemo, useCallback, FormEventHandler } from 'react'; - import useObservable from 'react-use/lib/useObservable'; -import { FormattedMessage } from '@kbn/i18n-react'; + import { EuiSpacer, EuiButton, EuiTabs, EuiTab, EuiForm } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { ErrorMessage } from '../../inference_error'; -import { extractErrorMessage } from '../../../../../../common'; import type { InferrerType } from '..'; import { OutputLoadingContent } from '../../output_loading'; import { RUNNING_STATE } from '../inference_base'; diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 2cfcf50c565144..c24ae573a95145 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -16,6 +16,7 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { each, get } from 'lodash'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import type { ErrorType } from '@kbn/ml-error-utils'; import { Dictionary } from '../../../../common/types/common'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; import { Datafeed, JobId } from '../../../../common/types/anomaly_detection_jobs'; @@ -28,7 +29,6 @@ import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { InfluencersFilterQuery } from '../../../../common/types/es_client'; import { RecordForInfluencer } from './results_service'; import { isRuntimeMappings } from '../../../../common'; -import { ErrorType } from '../../../../common/util/errors'; export interface ResultResponse { success: boolean; diff --git a/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts b/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts index 8bc7bc8c87e35e..585b881010d7a0 100644 --- a/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts +++ b/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts @@ -8,13 +8,9 @@ import { i18n } from '@kbn/i18n'; import { ToastInput, ToastOptions, ToastsStart } from '@kbn/core/public'; import { useMemo } from 'react'; +import { extractErrorProperties, type ErrorType, MLRequestFailure } from '@kbn/ml-error-utils'; import { getToastNotifications } from '../../util/dependency_cache'; import { useNotifications } from '../../contexts/kibana'; -import { - ErrorType, - extractErrorProperties, - MLRequestFailure, -} from '../../../../common/util/errors'; export type ToastNotificationService = ReturnType; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js index e97cf8e2639fc0..a767d7b65e4f35 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js @@ -5,10 +5,11 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { getToastNotifications } from '../../../util/dependency_cache'; import { ml } from '../../../services/ml_api_service'; -import { i18n } from '@kbn/i18n'; -import { extractErrorMessage } from '../../../../../common/util/errors'; export async function deleteCalendars(calendarsToDelete, callback) { if (calendarsToDelete === undefined || calendarsToDelete.length === 0) { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 64bc1eefccd795..3342680070e8b3 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -16,9 +16,13 @@ import React, { Component } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { withKibana } from '@kbn/kibana-react-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../../common/constants/states'; import { MESSAGE_LEVEL } from '../../../../../common/constants/message_levels'; -import { extractErrorMessage } from '../../../../../common/util/errors'; import { isJobVersionGte } from '../../../../../common/util/job_utils'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { Modal } from './modal'; @@ -26,9 +30,6 @@ import { PROGRESS_STATES } from './progress_states'; import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { mlForecastService } from '../../../services/forecast_service'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { withKibana } from '@kbn/kibana-react-plugin/public'; export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx index c9ca8250bedd99..af42229d8ac790 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx @@ -7,11 +7,11 @@ import React, { FC, useEffect, useState, useCallback, useContext } from 'react'; import { i18n } from '@kbn/i18n'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { MlTooltipComponent } from '../../../components/chart_tooltip'; import { TimeseriesChart } from './timeseries_chart'; import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search'; -import { extractErrorMessage } from '../../../../../common/util/errors'; import { Annotation } from '../../../../../common/types/annotations'; import { useMlKibana, useNotifications } from '../../../contexts/kibana'; import { getBoundsRoundedToInterval } from '../../../util/time_buckets'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index f9dd1fa94c4f0e..7a7837ba714353 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -7,9 +7,9 @@ import { forkJoin, Observable, of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { ml } from '../../services/ml_api_service'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../common/constants/search'; -import { extractErrorMessage } from '../../../../common/util/errors'; import { mlTimeSeriesSearchService } from '../timeseries_search_service'; import { mlResultsService, CriteriaField } from '../../services/results_service'; import { Job } from '../../../../common/types/anomaly_detection_jobs'; diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx index 7585ffe32118d9..e2fff3bd286cf3 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx @@ -5,14 +5,10 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import React, { FC, useState, useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import useDebounce from 'react-use/lib/useDebounce'; -import type { Embeddable } from '@kbn/lens-plugin/public'; -import type { MapEmbeddable } from '@kbn/maps-plugin/public'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiFlexGroup, EuiFlexItem, @@ -30,11 +26,16 @@ import { EuiCallOut, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { Embeddable } from '@kbn/lens-plugin/public'; +import type { MapEmbeddable } from '@kbn/maps-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { QuickLensJobCreator } from '../../../application/jobs/new_job/job_from_lens'; import type { LayerResult } from '../../../application/jobs/new_job/job_from_lens'; import type { CreateState } from '../../../application/jobs/new_job/job_from_dashboard'; import { JOB_TYPE, DEFAULT_BUCKET_SPAN } from '../../../../common/constants/new_job'; -import { extractErrorMessage } from '../../../../common/util/errors'; import { basicJobValidation } from '../../../../common/util/job_utils'; import { JOB_ID_MAX_LENGTH } from '../../../../common/constants/validation'; import { invalidTimeIntervalMessage } from '../../../application/jobs/new_job/common/job_validator/util'; diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/incompatible_layer.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/incompatible_layer.tsx index f012e928e6b616..5f804e209eaa3e 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/incompatible_layer.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/incompatible_layer.tsx @@ -6,13 +6,13 @@ */ import React, { FC } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; -import type { LayerResult } from '../../../../../application/jobs/new_job/job_from_lens'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; +import type { LayerResult } from '../../../../../application/jobs/new_job/job_from_lens'; interface Props { layer: LayerResult; diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts index f247be10047181..1d19fee5c8392c 100644 --- a/x-pack/plugins/ml/public/shared.ts +++ b/x-pack/plugins/ml/public/shared.ts @@ -15,7 +15,6 @@ export * from '../common/types/modules'; export * from '../common/types/audit_message'; export * from '../common/util/anomaly_utils'; -export * from '../common/util/errors'; export * from '../common/util/validators'; export * from '../common/util/date_utils'; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 6d4982ecbf4647..9d286738b17da2 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient } from '@kbn/core/server'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { getAnalysisType } from '../../../common/util/analytics_utils'; import { ANALYSIS_CONFIG_TYPE } from '../../../common/constants/data_frame_analytics'; import { @@ -25,7 +26,6 @@ import { isRegressionAnalysis, isClassificationAnalysis, } from '../../../common/util/analytics_utils'; -import { extractErrorMessage } from '../../../common/util/errors'; import { AnalysisConfig, DataFrameAnalyticsConfig, diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 6179571a285157..8288c9998fc7db 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -30,7 +30,6 @@ "@kbn/charts-plugin", "@kbn/cloud-plugin", "@kbn/config-schema", - "@kbn/core-http-browser", "@kbn/dashboard-plugin", "@kbn/data-plugin", "@kbn/data-views-plugin", @@ -90,5 +89,6 @@ "@kbn/unified-search-plugin", "@kbn/usage-collection-plugin", "@kbn/utility-types", + "@kbn/ml-error-utils", ], } diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/api/ml_anomaly.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/api/ml_anomaly.ts index 18ce74d9823bb1..ae069537e1b267 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/state/api/ml_anomaly.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/api/ml_anomaly.ts @@ -11,7 +11,7 @@ import { JobExistResult, MlCapabilitiesResponse, } from '@kbn/ml-plugin/public'; -import { extractErrorMessage } from '@kbn/ml-plugin/common'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; import { API_URLS, ML_MODULE_ID } from '../../../../common/constants'; diff --git a/x-pack/plugins/synthetics/tsconfig.json b/x-pack/plugins/synthetics/tsconfig.json index a02e14d577b35a..61b7fe8d8d19c2 100644 --- a/x-pack/plugins/synthetics/tsconfig.json +++ b/x-pack/plugins/synthetics/tsconfig.json @@ -78,6 +78,7 @@ "@kbn/alerts-as-data-utils", "@kbn/exploratory-view-plugin", "@kbn/observability-shared-plugin", + "@kbn/ml-error-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx index f90faf53e87b51..25318fc9e2903d 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import type { DeleteTransformStatus, DeleteTransformsRequestSchema, @@ -24,7 +25,6 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { const { http, data: { dataViews: dataViewsContract }, - ml: { extractErrorMessage }, application: { capabilities }, } = useAppDependencies(); const toastNotifications = useToastNotifications(); @@ -62,7 +62,7 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { ); } }, - [dataViewsContract, toastNotifications, extractErrorMessage] + [dataViewsContract, toastNotifications] ); const checkUserIndexPermission = useCallback(async () => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx index c23d6ed475efc5..e0a52978c0b4a9 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx @@ -25,8 +25,8 @@ interface SourceSearchBarProps { } export const SourceSearchBar: FC = ({ dataView, searchBar }) => { const { - actions: { searchChangeHandler, searchSubmitHandler, setErrorMessage }, - state: { errorMessage, searchInput }, + actions: { searchChangeHandler, searchSubmitHandler, setQueryErrorMessage }, + state: { queryErrorMessage, searchInput }, } = searchBar; const { @@ -44,7 +44,7 @@ export const SourceSearchBar: FC = ({ dataView, searchBar return ( setErrorMessage(undefined)} + closePopover={() => setQueryErrorMessage(undefined)} input={ = ({ dataView, searchBar }} /> } - isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + isOpen={queryErrorMessage?.query === searchInput.query && queryErrorMessage?.message !== ''} > {i18n.translate('xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar', { - defaultMessage: 'Invalid query: {errorMessage}', + defaultMessage: 'Invalid query: {queryErrorMessage}', values: { - errorMessage: errorMessage?.message.split('\n')[0], + queryErrorMessage: queryErrorMessage?.message.split('\n')[0], }, })} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts index 775401decef352..157ab0051e6312 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts @@ -13,4 +13,4 @@ export { getDefaultAggregationConfig } from './get_default_aggregation_config'; export { getDefaultGroupByConfig } from './get_default_group_by_config'; export { getDefaultStepDefineState } from './get_default_step_define_state'; export { getPivotDropdownOptions } from './get_pivot_dropdown_options'; -export type { ErrorMessage, Field, StepDefineExposedState } from './types'; +export type { Field, StepDefineExposedState } from './types'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index 682001a937381d..2e66b2187a9e02 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -28,11 +28,6 @@ import { import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms'; import { RUNTIME_FIELD_TYPES } from '../../../../../../../common/shared_imports'; -export interface ErrorMessage { - query: string; - message: string; -} - export interface Field { name: EsFieldName; type: KBN_FIELD_TYPES | TIME_SERIES_METRIC_TYPES.COUNTER; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts index e8d56fc0029814..62f8661f0ca49d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts @@ -9,11 +9,11 @@ import { useState } from 'react'; import { toElasticsearchQuery, fromKueryExpression, luceneStringToDsl } from '@kbn/es-query'; import type { Query } from '@kbn/es-query'; +import type { QueryErrorMessage } from '@kbn/ml-error-utils'; import { getTransformConfigQuery } from '../../../../../common'; import { - ErrorMessage, StepDefineExposedState, QUERY_LANGUAGE_KUERY, QUERY_LANGUAGE_LUCENE, @@ -43,7 +43,9 @@ export const useSearchBar = ( const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); - const [errorMessage, setErrorMessage] = useState(undefined); + const [queryErrorMessage, setQueryErrorMessage] = useState( + undefined + ); const searchChangeHandler = (query: Query) => setSearchInput(query); const searchSubmitHandler = (query: Query) => { @@ -61,7 +63,7 @@ export const useSearchBar = ( return; } } catch (e) { - setErrorMessage({ query: query.query as string, message: e.message }); + setQueryErrorMessage({ query: query.query as string, message: e.message }); } }; @@ -71,14 +73,14 @@ export const useSearchBar = ( actions: { searchChangeHandler, searchSubmitHandler, - setErrorMessage, + setQueryErrorMessage, setSearchInput, setSearchLanguage, setSearchQuery, setSearchString, }, state: { - errorMessage, + queryErrorMessage, transformConfigQuery, searchInput, searchLanguage, diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index 6f02b1978506fe..9054ee7aa3b23f 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -142,7 +142,7 @@ export function wrapEsError(err: any, statusCodeToMessageMap: Record Date: Sun, 23 Apr 2023 11:51:44 +0300 Subject: [PATCH 23/65] [Cloud Posture] Dashboard initial FTR (#155163) --- ...e_chart.tsx => compliance_score_chart.tsx} | 14 +- .../compliance_dashboard.tsx | 2 + .../dashboard_sections/benchmarks_section.tsx | 4 +- .../dashboard_sections/summary_section.tsx | 7 +- .../compliance_dashboard/test_subjects.ts | 7 + .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../page_objects/csp_dashboard_page.ts | 134 ++++++++++++++++++ .../page_objects/index.ts | 2 + .../pages/compliance_dashboard.ts | 64 +++++++++ .../pages/index.ts | 1 + 12 files changed, 224 insertions(+), 17 deletions(-) rename x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/{cloud_posture_score_chart.tsx => compliance_score_chart.tsx} (91%) create mode 100644 x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts create mode 100644 x-pack/test/cloud_security_posture_functional/pages/compliance_dashboard.ts diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx similarity index 91% rename from x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx rename to x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx index c1b86258a46c95..c15e0ce87570f4 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx @@ -29,13 +29,14 @@ import { import { FormattedDate, FormattedTime } from '@kbn/i18n-react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { DASHBOARD_COMPLIANCE_SCORE_CHART } from '../test_subjects'; import { statusColors } from '../../../common/constants'; import { RULE_FAILED, RULE_PASSED } from '../../../../common/constants'; import { CompactFormattedNumber } from '../../../components/compact_formatted_number'; import type { Evaluation, PostureTrend, Stats } from '../../../../common/types'; import { useKibana } from '../../../common/hooks/use_kibana'; -interface CloudPostureScoreChartProps { +interface ComplianceScoreChartProps { compact?: boolean; trend: PostureTrend[]; data: Stats; @@ -48,7 +49,7 @@ const getPostureScorePercentage = (postureScore: number): string => `${Math.roun const PercentageInfo = ({ compact, postureScore, -}: CloudPostureScoreChartProps['data'] & { compact?: CloudPostureScoreChartProps['compact'] }) => { +}: ComplianceScoreChartProps['data'] & { compact?: ComplianceScoreChartProps['compact'] }) => { const { euiTheme } = useEuiTheme(); const percentage = getPostureScorePercentage(postureScore); @@ -59,6 +60,7 @@ const PercentageInfo = ({ paddingLeft: compact ? euiTheme.size.s : euiTheme.size.xs, marginBottom: compact ? euiTheme.size.s : 'none', }} + data-test-subj={DASHBOARD_COMPLIANCE_SCORE_CHART.COMPLIANCE_SCORE} >

{percentage}

@@ -140,12 +142,12 @@ const CounterLink = ({ ); }; -export const CloudPostureScoreChart = ({ +export const ComplianceScoreChart = ({ data, trend, onEvalCounterClick, compact, -}: CloudPostureScoreChartProps) => { +}: ComplianceScoreChartProps) => { const { euiTheme } = useEuiTheme(); return ( @@ -173,7 +175,7 @@ export const CloudPostureScoreChart = ({ color={statusColors.passed} onClick={() => onEvalCounterClick(RULE_PASSED)} tooltipContent={i18n.translate( - 'xpack.csp.cloudPostureScoreChart.counterLink.passedFindingsTooltip', + 'xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip', { defaultMessage: 'Passed findings' } )} /> @@ -184,7 +186,7 @@ export const CloudPostureScoreChart = ({ color={statusColors.failed} onClick={() => onEvalCounterClick(RULE_FAILED)} tooltipContent={i18n.translate( - 'xpack.csp.cloudPostureScoreChart.counterLink.failedFindingsTooltip', + 'xpack.csp.complianceScoreChart.counterLink.failedFindingsTooltip', { defaultMessage: 'Failed findings' } )} /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 6c75891fc62af4..fe4f62dbbc8f5a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -31,6 +31,7 @@ import { KUBERNETES_DASHBOARD_CONTAINER, KUBERNETES_DASHBOARD_TAB, CLOUD_DASHBOARD_TAB, + CLOUD_POSTURE_DASHBOARD_PAGE_HEADER, } from './test_subjects'; import { useCspmStatsApi, useKspmStatsApi } from '../../common/api/use_stats_api'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; @@ -332,6 +333,7 @@ export const ComplianceDashboard = () => { return ( - @@ -151,7 +152,7 @@ export const SummarySection = ({ - => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const index = { + remove: () => es.indices.delete({ index: LATEST_FINDINGS_INDEX, ignore_unavailable: true }), + add: async (findingsMock: T[]) => { + await Promise.all( + findingsMock.map((finding) => + es.index({ + index: LATEST_FINDINGS_INDEX, + body: finding, + }) + ) + ); + }, + }; + + const dashboard = { + getDashboardPageHeader: () => testSubjects.find('cloud-posture-dashboard-page-header'), + + getDashboardTabs: async () => { + const dashboardPageHeader = await dashboard.getDashboardPageHeader(); + return await dashboardPageHeader.findByClassName('euiTabs'); + }, + + getCloudTab: async () => { + const tabs = await dashboard.getDashboardTabs(); + return await tabs.findByXpath(`//span[text()="Cloud"]`); + }, + + getKubernetesTab: async () => { + const tabs = await dashboard.getDashboardTabs(); + return await tabs.findByXpath(`//span[text()="Kubernetes"]`); + }, + + clickTab: async (tab: 'Cloud' | 'Kubernetes') => { + if (tab === 'Cloud') { + const cloudTab = await dashboard.getCloudTab(); + await cloudTab.click(); + } + if (tab === 'Kubernetes') { + const k8sTab = await dashboard.getKubernetesTab(); + await k8sTab.click(); + } + }, + + getIntegrationDashboardContainer: () => testSubjects.find('dashboard-container'), + + // Cloud Dashboard + + getCloudDashboard: async () => { + await dashboard.clickTab('Cloud'); + return await testSubjects.find('cloud-dashboard-container'); + }, + + getCloudSummarySection: async () => { + await dashboard.getCloudDashboard(); + return await testSubjects.find('dashboard-summary-section'); + }, + + getCloudComplianceScore: async () => { + await dashboard.getCloudSummarySection(); + return await testSubjects.find('dashboard-summary-section-compliance-score'); + }, + + // Kubernetes Dashboard + + getKubernetesDashboard: async () => { + await dashboard.clickTab('Kubernetes'); + return await testSubjects.find('kubernetes-dashboard-container'); + }, + + getKubernetesSummarySection: async () => { + await dashboard.getKubernetesDashboard(); + return await testSubjects.find('dashboard-summary-section'); + }, + + getKubernetesComplianceScore: async () => { + await dashboard.getKubernetesSummarySection(); + return await testSubjects.find('dashboard-summary-section-compliance-score'); + }, + + getKubernetesComplianceScore2: async () => { + // await dashboard.getKubernetesSummarySection(); + return await testSubjects.find('dashboard-summary-section-compliance-score'); + }, + }; + + const navigateToComplianceDashboardPage = async () => { + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/dashboard', + { shouldUseHashForSubUrl: false } + ); + }; + + return { + waitForPluginInitialized, + navigateToComplianceDashboardPage, + dashboard, + index, + }; +} diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts index e5738873edc51a..26aacd8cca997e 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts @@ -7,8 +7,10 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { FindingsPageProvider } from './findings_page'; +import { CspDashboardPageProvider } from './csp_dashboard_page'; export const pageObjects = { ...xpackFunctionalPageObjects, findings: FindingsPageProvider, + cloudPostureDashboard: CspDashboardPageProvider, }; diff --git a/x-pack/test/cloud_security_posture_functional/pages/compliance_dashboard.ts b/x-pack/test/cloud_security_posture_functional/pages/compliance_dashboard.ts new file mode 100644 index 00000000000000..cb4635899f5c38 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/compliance_dashboard.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import Chance from 'chance'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const retry = getService('retry'); + const pageObjects = getPageObjects(['common', 'cloudPostureDashboard']); + const chance = new Chance(); + + const data = [ + { + '@timestamp': new Date().toISOString(), + resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, + result: { evaluation: 'failed' }, + rule: { + name: 'Upper case rule name', + section: 'Upper case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + }, + }, + cluster_id: 'Upper case cluster id', + }, + ]; + + describe('Cloud Posture Dashboard Page', () => { + let cspDashboard: typeof pageObjects.cloudPostureDashboard; + let dashboard: typeof pageObjects.cloudPostureDashboard.dashboard; + + before(async () => { + cspDashboard = pageObjects.cloudPostureDashboard; + dashboard = pageObjects.cloudPostureDashboard.dashboard; + await cspDashboard.waitForPluginInitialized(); + + await cspDashboard.index.add(data); + await cspDashboard.navigateToComplianceDashboardPage(); + await retry.waitFor( + 'Cloud posture integration dashboard to be displayed', + async () => !!dashboard.getIntegrationDashboardContainer() + ); + }); + + after(async () => { + await cspDashboard.index.remove(); + }); + + describe('Kubernetes Dashboard', () => { + it('displays accurate summary compliance score', async () => { + const scoreElement = await dashboard.getKubernetesComplianceScore(); + + expect((await scoreElement.getVisibleText()) === '0%').to.be(true); + }); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts index 80e96b8b17ce95..7566afda0501a4 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/index.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -11,5 +11,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Cloud Security Posture', function () { loadTestFile(require.resolve('./findings')); + loadTestFile(require.resolve('./compliance_dashboard')); }); } From 68719bdb07d5307d567489b2b65a3aa8d59e630c Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Sun, 23 Apr 2023 10:53:48 +0200 Subject: [PATCH 24/65] [RAM][SECURITYSOLUTION][ALERTS] - Integrate Alert summary inside of security solution rule page (#154990) ## Summary [Main ticket](https://github.com/elastic/kibana/issues/151916) This PR dependant on [these changes](https://github.com/elastic/kibana/pull/153101) These changes cover next two tickets: - [RAM][SECURITYSOLUTION][ALERTS] - Integrate per-action frequency field in security solution APIs #154532 - [RAM][SECURITYSOLUTION][ALERTS] - Integrate per-action frequency UI in security solution #154534 With this PR we will integrate per-action `frequency` field which already works within alert framework and will update security solution UI to incorporate the possibility to select "summary" vs "for each alert" type of actions. ![](https://user-images.githubusercontent.com/616158/227377473-f34a330e-81ce-42b4-af1b-e6e302c6319d.png) ## NOTES: - There will be no more "Perform no actions" option which mutes all the actions of the rule. For back compatibility, we need to show that rule is muted in the UI cc @peluja1012 @ARWNightingale - The ability to generate per-alert action will be done as part of https://github.com/elastic/kibana/issues/153611 ## Technical Notes: Here are the overview of the conversions and transformations that we are going to do as part of these changes for devs who are going to review. On rule **create**/**read**/**update**/**patch**: - We always gonna set rule level `throttle` to `undefined` from now on - If each action has `frequency` attribute set, then we just use those values - If actions do not have `frequency` attribute set (or for some reason there is a mix of actions with some of them having `frequency` attribute and some not), then we transform rule level `throttle` into `frequency` and set it for each action in the rule On rule **bulk editing**: - We always gonna set rule level `throttle` to `undefined` - If each action has `frequency` attribute set, then we just use those values - If actions do not have `frequency` attribute set, then we transform rule level `throttle` into `frequency` and set it for each action in the rule - If user passed only `throttle` attribute with empty actions (`actions = []`), this will only remove all actions from the rule This will bring breaking changes which we agreed on with the Advanced Correlation Group cc @XavierM @vitaliidm @peluja1012 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Maxim Palenov --- .../index.ts | 1 + .../src/actions/index.ts | 13 +- .../src/frequency/index.ts | 37 ++++ .../src/throttle/index.ts | 2 +- .../alerting/server/routes/bulk_edit_rules.ts | 11 + .../format_legacy_actions.ts | 5 +- .../transform_legacy_actions.test.ts | 2 +- .../transform_legacy_actions.ts | 5 +- .../transform_to_alert_throttle.test.ts | 21 ++ .../transform_to_alert_throttle.ts | 20 ++ .../server/rules_client/methods/bulk_edit.ts | 15 +- .../server/rules_client/methods/create.ts | 13 -- .../server/rules_client/methods/update.ts | 14 +- .../security_solution/common/constants.ts | 11 + .../rules/bulk_actions/request_schema.test.ts | 22 -- .../api/rules/bulk_actions/request_schema.ts | 24 ++- .../rule_schema/model/rule_schemas.ts | 3 +- .../detection_engine/transform_actions.ts | 4 + .../bulk_edit_rules_actions.cy.ts | 47 +++-- .../detection_rules/custom_query_rule.cy.ts | 12 +- .../e2e/detection_rules/rule_actions.cy.ts | 2 +- .../security_solution/cypress/objects/rule.ts | 1 - .../cypress/objects/types.ts | 3 - .../cypress/screens/common/rule_actions.ts | 17 ++ .../cypress/screens/create_new_rule.ts | 3 - .../cypress/screens/rules_bulk_edit.ts | 3 - .../cypress/tasks/common/rule_actions.ts | 85 ++++++++ .../cypress/tasks/create_new_rule.ts | 2 - .../cypress/tasks/edit_rule.ts | 5 - .../cypress/tasks/rules_bulk_edit.ts | 5 - .../pages/rule_creation/helpers.test.ts | 86 -------- .../pages/rule_creation/helpers.ts | 10 +- .../components/rules_table/__mocks__/mock.ts | 1 - .../bulk_actions/forms/rule_actions_form.tsx | 48 +---- .../rules_table/bulk_actions/translations.tsx | 15 -- .../bulk_actions/use_bulk_actions.tsx | 11 - .../utils/compute_dry_run_edit_payload.ts | 2 +- .../rules/rule_actions_field/index.tsx | 31 ++- .../rules/step_rule_actions/get_schema.ts | 10 - .../rules/step_rule_actions/index.tsx | 93 ++------- .../rules/step_rule_actions/translations.tsx | 16 -- .../use_manage_case_action.tsx | 66 ------ .../detection_engine/rules/helpers.test.tsx | 30 ++- .../pages/detection_engine/rules/helpers.tsx | 3 +- .../pages/detection_engine/rules/types.ts | 1 - .../routes/__mocks__/utils.ts | 2 +- .../logic/actions/duplicate_rule.test.ts | 2 - .../logic/actions/duplicate_rule.ts | 6 +- .../action_to_rules_client_operation.ts | 25 +-- .../logic/crud/patch_rules.test.ts | 2 + .../logic/crud/update_rules.ts | 9 +- .../logic/export/get_export_all.test.ts | 4 +- .../export/get_export_by_object_ids.test.ts | 6 +- .../normalization/rule_actions.test.ts | 151 ++++++++++++++ .../normalization/rule_actions.ts | 34 ++- .../normalization/rule_converters.ts | 39 ++-- .../rule_management/utils/validate.test.ts | 2 +- .../rule_schema/model/rule_schemas.ts | 12 +- .../translations/translations/fr-FR.json | 6 - .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../basic/tests/create_rules.ts | 1 - .../group1/create_rules.ts | 149 ++++++++++++- .../group1/create_rules_bulk.ts | 145 +++++++++++++ .../group1/delete_rules.ts | 1 + .../group1/delete_rules_bulk.ts | 3 + .../group1/export_rules.ts | 47 +++-- .../security_and_spaces/group1/find_rules.ts | 20 +- .../group10/legacy_actions_migrations.ts | 26 ++- .../group10/patch_rules.ts | 180 +++++++++++++++- .../group10/patch_rules_bulk.ts | 2 +- .../group10/perform_bulk_action.ts | 83 +++++--- .../security_and_spaces/group10/read_rules.ts | 20 +- .../security_and_spaces/group10/throttle.ts | 66 +++--- .../group10/update_rules.ts | 195 +++++++++++++++++- .../group10/update_rules_bulk.ts | 195 +++++++++++++++++- .../utils/get_complex_rule_output.ts | 1 - .../utils/get_rule_actions.ts | 96 +++++++++ .../utils/get_simple_rule_output.ts | 2 +- ...simple_rule_output_with_web_hook_action.ts | 3 +- .../utils/remove_uuid_from_actions.ts | 14 ++ 81 files changed, 1720 insertions(+), 672 deletions(-) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts create mode 100644 x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts create mode 100644 x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/index.ts index 7a200e4f4c8f90..a1d34a9f944513 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/index.ts @@ -20,6 +20,7 @@ export * from './src/default_severity_mapping_array'; export * from './src/default_threat_array'; export * from './src/default_to_string'; export * from './src/default_uuid'; +export * from './src/frequency'; export * from './src/language'; export * from './src/machine_learning_job_id'; export * from './src/max_signals'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts index ab6df3bdacc2aa..1ca2f7fceba40c 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts @@ -9,6 +9,7 @@ import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import * as t from 'io-ts'; import { saved_object_attributes } from '../saved_object_attributes'; +import { RuleActionFrequency } from '../frequency'; export type RuleActionGroup = t.TypeOf; export const RuleActionGroup = t.string; @@ -94,7 +95,11 @@ export const RuleAction = t.exact( action_type_id: RuleActionTypeId, params: RuleActionParams, }), - t.partial({ uuid: RuleActionUuid, alerts_filter: RuleActionAlertsFilter }), + t.partial({ + uuid: RuleActionUuid, + alerts_filter: RuleActionAlertsFilter, + frequency: RuleActionFrequency, + }), ]) ); @@ -110,7 +115,11 @@ export const RuleActionCamel = t.exact( actionTypeId: RuleActionTypeId, params: RuleActionParams, }), - t.partial({ uuid: RuleActionUuid, alertsFilter: RuleActionAlertsFilter }), + t.partial({ + uuid: RuleActionUuid, + alertsFilter: RuleActionAlertsFilter, + frequency: RuleActionFrequency, + }), ]) ); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts new file mode 100644 index 00000000000000..ba6709d1ff495f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { RuleActionThrottle } from '../throttle'; + +/** + * Action summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert + */ +export type RuleActionSummary = t.TypeOf; +export const RuleActionSummary = t.boolean; + +/** + * The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval` + */ +export type RuleActionNotifyWhen = t.TypeOf; +export const RuleActionNotifyWhen = t.union([ + t.literal('onActionGroupChange'), + t.literal('onActiveAlert'), + t.literal('onThrottleInterval'), +]); + +/** + * The action frequency defines when the action runs (for example, only on rule execution or at specific time intervals). + */ +export type RuleActionFrequency = t.TypeOf; +export const RuleActionFrequency = t.type({ + summary: RuleActionSummary, + notifyWhen: RuleActionNotifyWhen, + throttle: t.union([RuleActionThrottle, t.null]), +}); diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts index fbc75ca67693e9..c0fb60482a91cf 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts @@ -13,5 +13,5 @@ export type RuleActionThrottle = t.TypeOf; export const RuleActionThrottle = t.union([ t.literal('no_actions'), t.literal('rule'), - TimeDuration({ allowedUnits: ['h', 'd'] }), + TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] }), ]); diff --git a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts index fa30b0ff8d2ed8..f22d7d2055b091 100644 --- a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts +++ b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts @@ -19,6 +19,17 @@ const ruleActionSchema = schema.object({ id: schema.string(), params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), uuid: schema.maybe(schema.string()), + frequency: schema.maybe( + schema.object({ + summary: schema.boolean(), + throttle: schema.nullable(schema.string()), + notifyWhen: schema.oneOf([ + schema.literal('onActionGroupChange'), + schema.literal('onActiveAlert'), + schema.literal('onThrottleInterval'), + ]), + }) + ), }); const operationsSchema = schema.arrayOf( diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts index 8689113ca79072..a0aa3286f1f6de 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts @@ -14,6 +14,7 @@ import { injectReferencesIntoActions } from '../../common'; import { transformToNotifyWhen } from './transform_to_notify_when'; import { transformFromLegacyActions } from './transform_legacy_actions'; import { LegacyIRuleActionsAttributes, legacyRuleActionsSavedObjectType } from './types'; +import { transformToAlertThrottle } from './transform_to_alert_throttle'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -136,7 +137,9 @@ export const formatLegacyActions = async ( return { ...rule, actions: [...rule.actions, ...legacyRuleActions], - throttle: (legacyRuleActions.length ? ruleThrottle : rule.throttle) ?? 'no_actions', + throttle: transformToAlertThrottle( + (legacyRuleActions.length ? ruleThrottle : rule.throttle) ?? 'no_actions' + ), notifyWhen: transformToNotifyWhen(ruleThrottle), // muteAll property is disregarded in further rule processing in Security Solution when legacy actions are present. // So it should be safe to set it as false, so it won't be displayed to user as w/o actions see transformFromAlertThrottle method diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts index b23798294b300b..cea32ce3e19136 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts @@ -67,7 +67,7 @@ describe('transformFromLegacyActions', () => { (transformToNotifyWhen as jest.Mock).mockReturnValueOnce(null); const actions = transformFromLegacyActions(legacyActionsAttr, references); - expect(actions[0].frequency?.notifyWhen).toBe('onThrottleInterval'); + expect(actions[0].frequency?.notifyWhen).toBe('onActiveAlert'); }); it('should return transformed legacy actions', () => { diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts index 485a32f7816952..1218e96a8dfd7c 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts @@ -11,6 +11,7 @@ import type { SavedObjectReference } from '@kbn/core/server'; import { RawRuleAction } from '../../../types'; import { transformToNotifyWhen } from './transform_to_notify_when'; import { LegacyIRuleActionsAttributes } from './types'; +import { transformToAlertThrottle } from './transform_to_alert_throttle'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -50,8 +51,8 @@ export const transformFromLegacyActions = ( actionTypeId, frequency: { summary: true, - notifyWhen: transformToNotifyWhen(legacyActionsAttr.ruleThrottle) ?? 'onThrottleInterval', - throttle: legacyActionsAttr.ruleThrottle, + notifyWhen: transformToNotifyWhen(legacyActionsAttr.ruleThrottle) ?? 'onActiveAlert', + throttle: transformToAlertThrottle(legacyActionsAttr.ruleThrottle), }, }, ]; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts new file mode 100644 index 00000000000000..f52742009503a4 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformToAlertThrottle } from './transform_to_alert_throttle'; + +describe('transformToAlertThrottle', () => { + it('should return null when throttle is null OR no_actions', () => { + expect(transformToAlertThrottle(null)).toBeNull(); + expect(transformToAlertThrottle('rule')).toBeNull(); + expect(transformToAlertThrottle('no_actions')).toBeNull(); + }); + it('should return same value for other throttle values', () => { + expect(transformToAlertThrottle('1h')).toBe('1h'); + expect(transformToAlertThrottle('1m')).toBe('1m'); + expect(transformToAlertThrottle('1d')).toBe('1d'); + }); +}); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts new file mode 100644 index 00000000000000..7f6ecee900e2fb --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Given a throttle from a "security_solution" rule this will transform it into an "alerting" "throttle" + * on their saved object. + * @params throttle The throttle from a "security_solution" rule + * @returns The "alerting" throttle + */ +export const transformToAlertThrottle = (throttle: string | null | undefined): string | null => { + if (throttle == null || throttle === 'rule' || throttle === 'no_actions') { + return null; + } else { + return throttle; + } +}; diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index ce11a0cc017e31..640f3c3f324b30 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -7,7 +7,7 @@ import pMap from 'p-map'; import Boom from '@hapi/boom'; -import { cloneDeep, omit } from 'lodash'; +import { cloneDeep } from 'lodash'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { @@ -638,19 +638,6 @@ async function getUpdatedAttributesFromOperations( isAttributesUpdateSkipped = false; } - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = updatedOperation.value.find( - (action) => action?.frequency - )?.frequency; - if (rule.attributes.consumer === AlertConsumers.SIEM && firstFrequency) { - ruleActions.actions = ruleActions.actions.map((action) => omit(action, 'frequency')); - if (!attributes.notifyWhen) { - attributes.notifyWhen = firstFrequency.notifyWhen; - attributes.throttle = firstFrequency.throttle; - } - } - break; } case 'snoozeSchedule': { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts index 90d6a4c49f90ab..fd83a7b9b92e1e 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/create.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts @@ -6,8 +6,6 @@ */ import Semver from 'semver'; import Boom from '@hapi/boom'; -import { omit } from 'lodash'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import { SavedObjectsUtils } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; import { parseDuration } from '../../../common/parse_duration'; @@ -111,17 +109,6 @@ export async function create( throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); } - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = data.actions.find((action) => action?.frequency)?.frequency; - if (data.consumer === AlertConsumers.SIEM && firstFrequency) { - data.actions = data.actions.map((action) => omit(action, 'frequency')); - if (!data.notifyWhen) { - data.notifyWhen = firstFrequency.notifyWhen; - data.throttle = firstFrequency.throttle; - } - } - await withSpan({ name: 'validateActions', type: 'rules' }, () => validateActions(context, ruleType, data, allowMissingConnectorSecrets) ); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts index 7c1fbedff37cee..f68b41067ddaed 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts @@ -6,9 +6,8 @@ */ import Boom from '@hapi/boom'; -import { isEqual, omit } from 'lodash'; +import { isEqual } from 'lodash'; import { SavedObject } from '@kbn/core/server'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import type { ShouldIncrementRevision } from './bulk_edit'; import { PartialRule, @@ -186,17 +185,6 @@ async function updateAlert( const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId); - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = data.actions.find((action) => action?.frequency)?.frequency; - if (attributes.consumer === AlertConsumers.SIEM && firstFrequency) { - data.actions = data.actions.map((action) => omit(action, 'frequency')); - if (!attributes.notifyWhen) { - attributes.notifyWhen = firstFrequency.notifyWhen; - attributes.throttle = firstFrequency.throttle; - } - } - // Validate const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate.params); await validateActions(context, ruleType, data, allowMissingConnectorSecrets); diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index aba27d9b617d52..2551bc34f7fa44 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; + /** * as const * @@ -377,9 +379,18 @@ export const ML_GROUP_ID = 'security' as const; export const LEGACY_ML_GROUP_ID = 'siem' as const; export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID] as const; +/** + * Rule Actions + */ export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions' as const; export const NOTIFICATION_THROTTLE_RULE = 'rule' as const; +export const NOTIFICATION_DEFAULT_FREQUENCY = { + notifyWhen: RuleNotifyWhen.ACTIVE, + throttle: null, + summary: true, +}; + export const showAllOthersBucket: string[] = [ 'destination.ip', 'event.action', diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts index 4b95327810fa92..712312c50140d0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts @@ -515,28 +515,6 @@ describe('Perform bulk action request schema', () => { expect(message.schema).toEqual({}); }); - test('invalid request: missing throttle in payload', () => { - const payload = { - query: 'name: test', - action: BulkActionType.edit, - [BulkActionType.edit]: [ - { - type: BulkActionEditType.add_rule_actions, - value: { - actions: [], - }, - }, - ], - }; - - const message = retrieveValidationMessage(payload); - - expect(getPaths(left(message.errors))).toEqual( - expect.arrayContaining(['Invalid value "undefined" supplied to "edit,value,throttle"']) - ); - expect(message.schema).toEqual({}); - }); - test('invalid request: missing actions in payload', () => { const payload = { query: 'name: test', diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts index 7b2838ab1d6575..e0a392885bad90 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { NonEmptyArray, TimeDuration } from '@kbn/securitysolution-io-ts-types'; import { + RuleActionFrequency, RuleActionGroup, RuleActionId, RuleActionParams, @@ -96,11 +97,14 @@ const BulkActionEditPayloadTimeline = t.type({ */ type NormalizedRuleAction = t.TypeOf; const NormalizedRuleAction = t.exact( - t.type({ - group: RuleActionGroup, - id: RuleActionId, - params: RuleActionParams, - }) + t.intersection([ + t.type({ + group: RuleActionGroup, + id: RuleActionId, + params: RuleActionParams, + }), + t.partial({ frequency: RuleActionFrequency }), + ]) ); export type BulkActionEditPayloadRuleActions = t.TypeOf; @@ -109,10 +113,12 @@ export const BulkActionEditPayloadRuleActions = t.type({ t.literal(BulkActionEditType.add_rule_actions), t.literal(BulkActionEditType.set_rule_actions), ]), - value: t.type({ - throttle: ThrottleForBulkActions, - actions: t.array(NormalizedRuleAction), - }), + value: t.intersection([ + t.partial({ throttle: ThrottleForBulkActions }), + t.type({ + actions: t.array(NormalizedRuleAction), + }), + ]), }); type BulkActionEditPayloadSchedule = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts index e0b427cdcefbc2..ff4bb72a5eb657 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts @@ -119,6 +119,8 @@ export const baseSchema = buildRuleSchemas({ output_index: AlertsIndex, namespace: AlertsIndexNamespace, meta: RuleMetadata, + // Throttle + throttle: RuleActionThrottle, }, defaultable: { // Main attributes @@ -134,7 +136,6 @@ export const baseSchema = buildRuleSchemas({ to: RuleIntervalTo, // Rule actions actions: RuleActionArray, - throttle: RuleActionThrottle, // Rule exceptions exceptions_list: ExceptionListArray, // Misc attributes diff --git a/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts b/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts index 1b74bcd320aad3..3808837dc0df29 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts @@ -16,6 +16,7 @@ export const transformRuleToAlertAction = ({ action_type_id: actionTypeId, params, uuid, + frequency, alerts_filter: alertsFilter, }: RuleAlertAction): RuleAction => ({ group, @@ -24,6 +25,7 @@ export const transformRuleToAlertAction = ({ actionTypeId, ...(alertsFilter && { alertsFilter }), ...(uuid && { uuid }), + ...(frequency && { frequency }), }); export const transformAlertToRuleAction = ({ @@ -32,6 +34,7 @@ export const transformAlertToRuleAction = ({ actionTypeId, params, uuid, + frequency, alertsFilter, }: RuleAction): RuleAlertAction => ({ group, @@ -40,6 +43,7 @@ export const transformAlertToRuleAction = ({ action_type_id: actionTypeId, ...(alertsFilter && { alerts_filter: alertsFilter }), ...(uuid && { uuid }), + ...(frequency && { frequency }), }); export const transformRuleToAlertResponseAction = ({ diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts index fceecae2b1d5b2..624dec7f1c9555 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; import { ROLES } from '../../../common/test'; import { @@ -15,11 +16,18 @@ import { import { actionFormSelector } from '../../screens/common/rule_actions'; import { cleanKibana, deleteAlertsAndRules, deleteConnectors } from '../../tasks/common'; +import type { RuleActionCustomFrequency } from '../../tasks/common/rule_actions'; import { addSlackRuleAction, assertSlackRuleAction, addEmailConnectorAndRuleAction, assertEmailRuleAction, + assertSelectedCustomFrequencyOption, + assertSelectedPerRuleRunFrequencyOption, + assertSelectedSummaryOfAlertsOption, + pickCustomFrequencyOption, + pickPerRuleRunFrequencyOption, + pickSummaryOfAlertsOption, } from '../../tasks/common/rule_actions'; import { waitForRulesTableToBeLoaded, @@ -32,10 +40,8 @@ import { submitBulkEditForm, checkOverwriteRuleActionsCheckbox, openBulkEditRuleActionsForm, - pickActionFrequency, openBulkActionsMenu, } from '../../tasks/rules_bulk_edit'; -import { assertSelectedActionFrequency } from '../../tasks/edit_rule'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { esArchiverResetKibana } from '../../tasks/es_archiver'; @@ -75,7 +81,7 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { esArchiverResetKibana(); createSlackConnector().then(({ body }) => { - const actions = [ + const actions: RuleActionArray = [ { id: body.id, action_type_id: '.slack', @@ -83,6 +89,11 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { params: { message: expectedExistingSlackMessage, }, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, }, ]; @@ -120,7 +131,10 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { }); it('Add a rule action to rules (existing connector)', () => { - const expectedActionFrequency = 'Daily'; + const expectedActionFrequency: RuleActionCustomFrequency = { + throttle: 1, + throttleUnit: 'd', + }; loadPrebuiltDetectionRulesFromHeaderBtn(); @@ -131,8 +145,9 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // ensure rule actions info callout displayed on the form cy.get(RULES_BULK_EDIT_ACTIONS_INFO).should('be.visible'); - pickActionFrequency(expectedActionFrequency); addSlackRuleAction(expectedSlackMessage); + pickSummaryOfAlertsOption(); + pickCustomFrequencyOption(expectedActionFrequency); submitBulkEditForm(); waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfRulesToBeEdited }); @@ -140,7 +155,8 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedCustomFrequencyOption(expectedActionFrequency, 1); assertSlackRuleAction(expectedExistingSlackMessage, 0); assertSlackRuleAction(expectedSlackMessage, 1); // ensure there is no third action @@ -148,16 +164,15 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { }); it('Overwrite rule actions in rules', () => { - const expectedActionFrequency = 'On each rule execution'; - loadPrebuiltDetectionRulesFromHeaderBtn(); // select both custom and prebuilt rules selectNumberOfRules(expectedNumberOfRulesToBeEdited); openBulkEditRuleActionsForm(); - pickActionFrequency(expectedActionFrequency); addSlackRuleAction(expectedSlackMessage); + pickSummaryOfAlertsOption(); + pickPerRuleRunFrequencyOption(); // check overwrite box, ensure warning is displayed checkOverwriteRuleActionsCheckbox(); @@ -171,22 +186,27 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedPerRuleRunFrequencyOption(); assertSlackRuleAction(expectedSlackMessage); // ensure existing action was overwritten cy.get(actionFormSelector(1)).should('not.exist'); }); it('Add a rule action to rules (new connector)', () => { - const expectedActionFrequency = 'Hourly'; + const expectedActionFrequency: RuleActionCustomFrequency = { + throttle: 2, + throttleUnit: 'h', + }; const expectedEmail = 'test@example.com'; const expectedSubject = 'Subject'; selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); openBulkEditRuleActionsForm(); - pickActionFrequency(expectedActionFrequency); addEmailConnectorAndRuleAction(expectedEmail, expectedSubject); + pickSummaryOfAlertsOption(); + pickCustomFrequencyOption(expectedActionFrequency); submitBulkEditForm(); waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); @@ -194,7 +214,8 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedCustomFrequencyOption(expectedActionFrequency, 1); assertEmailRuleAction(expectedEmail, expectedSubject); }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts index 8e2ae1b85cce70..4965072e7038d9 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts @@ -19,10 +19,13 @@ import { RULE_SWITCH, SEVERITY, } from '../../screens/alerts_detection_rules'; +import { + ACTIONS_NOTIFY_WHEN_BUTTON, + ACTIONS_SUMMARY_BUTTON, +} from '../../screens/common/rule_actions'; import { ABOUT_CONTINUE_BTN, ABOUT_EDIT_BUTTON, - ACTIONS_THROTTLE_INPUT, CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, DEFINE_EDIT_BUTTON, @@ -401,12 +404,11 @@ describe('Custom query rules', () => { goToActionsStepTab(); - cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions'); - - cy.get(ACTIONS_THROTTLE_INPUT).select('Weekly'); - addEmailConnectorAndRuleAction('test@example.com', 'Subject'); + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'Summary of alerts'); + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Per rule run'); + goToAboutStepTab(); cy.get(TAGS_CLEAR_BUTTON).click({ force: true }); fillAboutRule(getEditedRule()); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts index 5ed5ef8be059aa..ab458e12dca2d5 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts @@ -43,7 +43,7 @@ describe('Rule actions during detection rule creation', () => { }); const rule = getSimpleCustomQueryRule(); - const actions = { throttle: 'rule', connectors: [indexConnector] }; + const actions = { connectors: [indexConnector] }; const index = actions.connectors[0].index; const initialNumberOfDocuments = 0; const expectedJson = JSON.parse(actions.connectors[0].document); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index ac630d1ee0fd9f..af44aa14aa668c 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -578,7 +578,6 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response = Partial>; export interface Actions { - throttle: RuleActionThrottle; connectors: Connectors[]; } diff --git a/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts b/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts index 2fe606fc6bf645..9a1702b96d63cb 100644 --- a/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts @@ -41,3 +41,20 @@ export const INDEX_SELECTOR = "[data-test-subj='.index-siem-ActionTypeSelectOpti export const actionFormSelector = (position: number) => `[data-test-subj="alertActionAccordion-${position}"]`; + +export const ACTIONS_SUMMARY_BUTTON = '[data-test-subj="summaryOrPerRuleSelect"]'; + +export const ACTIONS_NOTIFY_WHEN_BUTTON = '[data-test-subj="notifyWhenSelect"]'; + +export const ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON = '[data-test-subj="onActiveAlert"]'; + +export const ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON = '[data-test-subj="onThrottleInterval"]'; + +export const ACTIONS_THROTTLE_INPUT = '[data-test-subj="throttleInput"]'; + +export const ACTIONS_THROTTLE_UNIT_INPUT = '[data-test-subj="throttleUnitInput"]'; + +export const ACTIONS_SUMMARY_ALERT_BUTTON = '[data-test-subj="actionNotifyWhen-option-summary"]'; + +export const ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON = + '[data-test-subj="actionNotifyWhen-option-for_each"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index a0cccb508ed661..b248cf06e1a0dd 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -13,9 +13,6 @@ export const ABOUT_EDIT_TAB = '[data-test-subj="edit-rule-about-tab"]'; export const ACTIONS_EDIT_TAB = '[data-test-subj="edit-rule-actions-tab"]'; -export const ACTIONS_THROTTLE_INPUT = - '[data-test-subj="stepRuleActions"] [data-test-subj="select"]'; - export const ADD_FALSE_POSITIVE_BTN = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts index 9546b5da8ad8dd..7a36c7fd7c74eb 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts @@ -66,9 +66,6 @@ export const UPDATE_SCHEDULE_LOOKBACK_INPUT = export const UPDATE_SCHEDULE_TIME_UNIT_SELECT = '[data-test-subj="timeType"]'; -export const RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT = - '[data-test-subj="bulkEditRulesRuleActionThrottle"] [data-test-subj="select"]'; - export const RULES_BULK_EDIT_ACTIONS_INFO = '[data-test-subj="bulkEditRulesRuleActionInfo"]'; export const RULES_BULK_EDIT_ACTIONS_WARNING = '[data-test-subj="bulkEditRulesRuleActionsWarning"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts b/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts index 2c289eea0f7365..a9a45780567aea 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts @@ -22,6 +22,15 @@ import { EMAIL_CONNECTOR_PASSWORD_INPUT, FORM_VALIDATION_ERROR, JSON_EDITOR, + ACTIONS_SUMMARY_BUTTON, + ACTIONS_NOTIFY_WHEN_BUTTON, + ACTIONS_THROTTLE_INPUT, + ACTIONS_THROTTLE_UNIT_INPUT, + ACTIONS_SUMMARY_ALERT_BUTTON, + ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON, + ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON, + actionFormSelector, + ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON, } from '../../screens/common/rule_actions'; import { COMBO_BOX_INPUT, COMBO_BOX_SELECTION } from '../../screens/common/controls'; import type { EmailConnector, IndexConnector } from '../../objects/connector'; @@ -84,3 +93,79 @@ export const fillIndexConnectorForm = (connector: IndexConnector = getIndexConne parseSpecialCharSequences: false, }); }; + +export interface RuleActionCustomFrequency { + throttle?: number; + throttleUnit?: 's' | 'm' | 'h' | 'd'; +} + +export const pickSummaryOfAlertsOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).click(); + }); + cy.get(ACTIONS_SUMMARY_ALERT_BUTTON).click(); +}; +export const pickForEachAlertOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).click(); + }); + cy.get(ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON).click(); +}; + +export const pickCustomFrequencyOption = ( + { throttle = 1, throttleUnit = 'h' }: RuleActionCustomFrequency, + index = 0 +) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).click(); + }); + cy.get(ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON).click(); + form.within(() => { + cy.get(ACTIONS_THROTTLE_INPUT).type(`{selectAll}${throttle}`); + cy.get(ACTIONS_THROTTLE_UNIT_INPUT).select(throttleUnit); + }); +}; + +export const pickPerRuleRunFrequencyOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).click(); + }); + cy.get(ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON).click(); +}; + +export const assertSelectedSummaryOfAlertsOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'Summary of alerts'); + }); +}; + +export const assertSelectedForEachAlertOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'For each alert'); + }); +}; + +export const assertSelectedCustomFrequencyOption = ( + { throttle = 1, throttleUnit = 'h' }: RuleActionCustomFrequency, + index = 0 +) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Custom frequency'); + cy.get(ACTIONS_THROTTLE_INPUT).should('have.value', throttle); + cy.get(ACTIONS_THROTTLE_UNIT_INPUT).should('have.value', throttleUnit); + }); +}; + +export const assertSelectedPerRuleRunFrequencyOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Per rule run'); + }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index b8274ed33c1201..1f3f051c6f4741 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -99,7 +99,6 @@ import { NEW_TERMS_HISTORY_SIZE, NEW_TERMS_HISTORY_TIME_TYPE, NEW_TERMS_INPUT_AREA, - ACTIONS_THROTTLE_INPUT, CONTINUE_BUTTON, CREATE_WITHOUT_ENABLING_BTN, RULE_INDICES, @@ -407,7 +406,6 @@ export const fillFrom = (from: RuleIntervalFrom = ruleFields.ruleIntervalFrom) = }; export const fillRuleAction = (actions: Actions) => { - cy.get(ACTIONS_THROTTLE_INPUT).select(actions.throttle); actions.connectors.forEach((connector) => { switch (connector.type) { case 'index': diff --git a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts index a016691328ffd6..42d5619c28a675 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts @@ -6,7 +6,6 @@ */ import { BACK_TO_RULE_DETAILS, EDIT_SUBMIT_BUTTON } from '../screens/edit_rule'; -import { ACTIONS_THROTTLE_INPUT } from '../screens/create_new_rule'; export const saveEditedRule = () => { cy.get(EDIT_SUBMIT_BUTTON).should('exist').click({ force: true }); @@ -17,7 +16,3 @@ export const goBackToRuleDetails = () => { cy.get(BACK_TO_RULE_DETAILS).should('exist').click(); cy.get(BACK_TO_RULE_DETAILS).should('not.exist'); }; - -export const assertSelectedActionFrequency = (frequency: string) => { - cy.get(ACTIONS_THROTTLE_INPUT).find('option:selected').should('have.text', frequency); -}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts index b2203d1b1202ab..b4bf088bafd6ac 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts @@ -39,7 +39,6 @@ import { UPDATE_SCHEDULE_LOOKBACK_INPUT, RULES_BULK_EDIT_SCHEDULES_WARNING, RULES_BULK_EDIT_OVERWRITE_ACTIONS_CHECKBOX, - RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT, } from '../screens/rules_bulk_edit'; import { SCHEDULE_DETAILS } from '../screens/rule_details'; @@ -292,7 +291,3 @@ export const assertRuleScheduleValues = ({ interval, lookback }: RuleSchedule) = cy.get('dd').eq(1).should('contain.text', lookback); }); }; - -export const pickActionFrequency = (frequency: string) => { - cy.get(RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT).select(frequency); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts index af6a82af1a9d57..8b7a9c62b15417 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts @@ -797,91 +797,6 @@ describe('helpers', () => { meta: { kibana_siem_app_url: 'http://localhost:5601/app/siem', }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for no_actions', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: 'no_actions', - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - actions: [], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for rule', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: 'rule', - actions: [ - { - group: 'default', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - actions: [ - { - group: mockStepData.actions[0].group, - id: mockStepData.actions[0].id, - action_type_id: mockStepData.actions[0].actionTypeId, - params: mockStepData.actions[0].params, - }, - ], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'rule', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for interval', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: '1d', - actions: [ - { - group: 'default', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - actions: [ - { - group: mockStepData.actions[0].group, - id: mockStepData.actions[0].id, - action_type_id: mockStepData.actions[0].actionTypeId, - params: mockStepData.actions[0].params, - }, - ], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: mockStepData.throttle, }; expect(result).toEqual(expected); @@ -913,7 +828,6 @@ describe('helpers', () => { meta: { kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, - throttle: 'no_actions', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts index 5442727561ce19..e61f55dbce4eca 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts @@ -25,7 +25,6 @@ import type { Type, } from '@kbn/securitysolution-io-ts-alerting-types'; import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../common/constants'; import { assertUnreachable } from '../../../../../common/utility_types'; import { transformAlertToRuleAction, @@ -563,19 +562,12 @@ export const formatAboutStepData = ( }; export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { - const { - actions = [], - responseActions, - enabled, - kibanaSiemAppUrl, - throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, - } = actionsStepData; + const { actions = [], responseActions, enabled, kibanaSiemAppUrl } = actionsStepData; return { actions: actions.map(transformAlertToRuleAction), response_actions: responseActions?.map(transformAlertToRuleResponseAction), enabled, - throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, meta: { kibana_siem_app_url: kibanaSiemAppUrl, }, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts index 40707a4307f279..487052fcbf2efd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts @@ -200,7 +200,6 @@ export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({ actions: [], kibanaSiemAppUrl: 'http://localhost:5601/app/siem', enabled, - throttle: 'no_actions', }); export const mockDefineStepRule = (): DefineStepRule => ({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx index 8b791ee2aece12..ff41017ac17713 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx @@ -23,21 +23,13 @@ import { Field, } from '../../../../../../shared_imports'; import { BulkActionEditType } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; -import type { - BulkActionEditPayload, - ThrottleForBulkActions, -} from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; -import { NOTIFICATION_THROTTLE_RULE } from '../../../../../../../common/constants'; +import type { BulkActionEditPayload } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; import { bulkAddRuleActions as i18n } from '../translations'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { - ThrottleSelectField, - THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, -} from '../../../../../../detections/components/rules/throttle_select_field'; import { getAllActionMessageParams } from '../../../../../../detections/pages/detection_engine/rules/helpers'; import { RuleActionsField } from '../../../../../../detections/components/rules/rule_actions_field'; @@ -45,19 +37,16 @@ import { debouncedValidateRuleActionsField } from '../../../../../../detections/ const CommonUseField = getUseField({ component: Field }); +type BulkActionsRuleAction = RuleAction & Required>; + export interface RuleActionsFormData { - throttle: ThrottleForBulkActions; - actions: RuleAction[]; + actions: BulkActionsRuleAction[]; overwrite: boolean; } const getFormSchema = ( actionTypeRegistry: ActionTypeRegistryContract ): FormSchema => ({ - throttle: { - label: i18n.THROTTLE_LABEL, - helpText: i18n.THROTTLE_HELP_TEXT, - }, actions: { validations: [ { @@ -75,7 +64,6 @@ const getFormSchema = ( }); const defaultFormData: RuleActionsFormData = { - throttle: NOTIFICATION_THROTTLE_RULE, actions: [], overwrite: false, }; @@ -108,7 +96,7 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction return; } - const { actions = [], throttle: throttleToSubmit, overwrite: overwriteValue } = data; + const { actions = [], overwrite: overwriteValue } = data; const editAction = overwriteValue ? BulkActionEditType.set_rule_actions : BulkActionEditType.add_rule_actions; @@ -117,23 +105,10 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction type: editAction, value: { actions: actions.map(({ actionTypeId, ...action }) => action), - throttle: throttleToSubmit, }, }); }, [form, onConfirm]); - const throttleFieldComponentProps = useMemo( - () => ({ - idAria: 'bulkEditRulesRuleActionThrottle', - 'data-test-subj': 'bulkEditRulesRuleActionThrottle', - hasNoInitialSelection: false, - euiFieldProps: { - options: THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, - }, - }), - [] - ); - const messageVariables = useMemo(() => getAllActionMessageParams(), []); return ( @@ -156,24 +131,11 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction } >
    -
  • - -
  • {i18n.RULE_VARIABLES_DETAIL}
- - - omit(a, 'frequency')); - } - startTransaction({ name: BULK_RULE_ACTIONS.EDIT }); const hideWarningToast = () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts index c8f49ebe4a6c69..f25b1331d766ab 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts @@ -50,7 +50,7 @@ export function computeDryRunEditPayload(editAction: BulkActionEditType): BulkAc return [ { type: editAction, - value: { throttle: '1h', actions: [] }, + value: { actions: [] }, }, ]; case BulkActionEditType.set_schedule: diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 019e20b25db1aa..084be38391a45a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -16,7 +16,6 @@ import type { ActionVariables, NotifyWhenSelectOptions, } from '@kbn/triggers-actions-ui-plugin/public'; -import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; import type { RuleAction, RuleActionAlertsFilterProperty, @@ -24,6 +23,7 @@ import type { } from '@kbn/alerting-plugin/common'; import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; +import { NOTIFICATION_DEFAULT_FREQUENCY } from '../../../../../common/constants'; import type { FieldHook } from '../../../../shared_imports'; import { useFormContext } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; @@ -33,12 +33,6 @@ import { FORM_ON_ACTIVE_ALERT_OPTION, } from './translations'; -const DEFAULT_FREQUENCY = { - notifyWhen: RuleNotifyWhen.ACTIVE, - throttle: null, - summary: true, -}; - const NOTIFY_WHEN_OPTIONS: NotifyWhenSelectOptions[] = [ { isSummaryOption: true, @@ -214,6 +208,23 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = [field] ); + const setActionFrequency = useCallback( + (key: string, value: RuleActionParam, index: number) => { + field.setValue((prevValue: RuleAction[]) => { + const updatedActions = [...prevValue]; + updatedActions[index] = { + ...updatedActions[index], + frequency: { + ...(updatedActions[index].frequency ?? NOTIFICATION_DEFAULT_FREQUENCY), + [key]: value, + }, + }; + return updatedActions; + }); + }, + [field] + ); + const actionForm = useMemo( () => getActionForm({ @@ -223,22 +234,22 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = setActionIdByIndex, setActions: setAlertActionsProperty, setActionParamsProperty, - setActionFrequencyProperty: () => {}, + setActionFrequencyProperty: setActionFrequency, setActionAlertsFilterProperty, featureId: SecurityConnectorFeatureId, defaultActionMessage: DEFAULT_ACTION_MESSAGE, defaultSummaryMessage: DEFAULT_ACTION_MESSAGE, hideActionHeader: true, - hideNotifyWhen: true, hasSummary: true, notifyWhenSelectOptions: NOTIFY_WHEN_OPTIONS, - defaultRuleFrequency: DEFAULT_FREQUENCY, + defaultRuleFrequency: NOTIFICATION_DEFAULT_FREQUENCY, showActionAlertsFilter: true, }), [ actions, getActionForm, messageVariables, + setActionFrequency, setActionIdByIndex, setActionParamsProperty, setAlertActionsProperty, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts index 858578f8a5d387..f16cac0eb923a7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; - import type { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public'; import { debouncedValidateRuleActionsField } from '../../../containers/detection_engine/rules/validate_rule_actions_field'; @@ -30,12 +28,4 @@ export const getSchema = ({ responseActions: {}, enabled: {}, kibanaSiemAppUrl: {}, - throttle: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', - { - defaultMessage: 'Actions frequency', - } - ), - }, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 58dd95bcac0dd9..86bbb7604add2a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -15,7 +15,6 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { findIndex } from 'lodash/fp'; import type { FC } from 'react'; import React, { memo, useCallback, useEffect, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -29,20 +28,13 @@ import { ResponseActionsForm } from '../../../../detection_engine/rule_response_ import type { RuleStepProps, ActionsStepRule } from '../../../pages/detection_engine/rules/types'; import { RuleStep } from '../../../pages/detection_engine/rules/types'; import { StepRuleDescription } from '../description_step'; -import { Form, UseField, useForm, useFormData } from '../../../../shared_imports'; +import { Form, UseField, useForm } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; -import { - ThrottleSelectField, - THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, - DEFAULT_THROTTLE_OPTION, -} from '../throttle_select_field'; import { RuleActionsField } from '../rule_actions_field'; import { useKibana } from '../../../../common/lib/kibana'; import { getSchema } from './get_schema'; import * as I18n from './translations'; import { APP_UI_ID } from '../../../../../common/constants'; -import { useManageCaseAction } from './use_manage_case_action'; -import { THROTTLE_FIELD_HELP_TEXT, THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY } from './translations'; interface StepRuleActionsProps extends RuleStepProps { defaultValues?: ActionsStepRule | null; @@ -55,23 +47,10 @@ export const stepActionsDefaultValue: ActionsStepRule = { actions: [], responseActions: [], kibanaSiemAppUrl: '', - throttle: DEFAULT_THROTTLE_OPTION.value, }; const GhostFormField = () => <>; -const getThrottleOptions = (throttle?: string | null) => { - // Add support for throttle options set by the API - if ( - throttle && - findIndex(['value', throttle], THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING) < 0 - ) { - return [...THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, { value: throttle, text: throttle }]; - } - - return THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING; -}; - const DisplayActionsHeader = () => { return ( <> @@ -99,7 +78,6 @@ const StepRuleActionsComponent: FC = ({ actionMessageParams, ruleType, }) => { - const [isLoadingCaseAction] = useManageCaseAction(); const { services: { application, @@ -127,11 +105,6 @@ const StepRuleActionsComponent: FC = ({ schema, }); const { getFields, getFormData, submit } = form; - const [{ throttle: formThrottle }] = useFormData({ - form, - watch: ['throttle'], - }); - const throttle = formThrottle || initialState.throttle; const handleSubmit = useCallback( (enabled: boolean) => { @@ -163,44 +136,20 @@ const StepRuleActionsComponent: FC = ({ }; }, [getData, setForm]); - const throttleOptions = useMemo(() => { - return getThrottleOptions(throttle); - }, [throttle]); - - const throttleFieldComponentProps = useMemo( - () => ({ - idAria: 'detectionEngineStepRuleActionsThrottle', - isDisabled: isLoading, - isLoading: isLoadingCaseAction, - dataTestSubj: 'detectionEngineStepRuleActionsThrottle', - hasNoInitialSelection: false, - helpText: isQueryRule(ruleType) - ? THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY - : THROTTLE_FIELD_HELP_TEXT, - euiFieldProps: { - options: throttleOptions, - }, - }), - [isLoading, isLoadingCaseAction, ruleType, throttleOptions] - ); - const displayActionsOptions = useMemo( - () => - throttle !== stepActionsDefaultValue.throttle ? ( - <> - - - - ) : ( - - ), - [throttle, actionMessageParams] + () => ( + <> + + + + ), + [actionMessageParams] ); const displayResponseActionsOptions = useMemo(() => { if (isQueryRule(ruleType)) { @@ -217,11 +166,6 @@ const StepRuleActionsComponent: FC = ({ return application.capabilities.actions.show ? ( <> - {displayActionsOptions} {responseActionsEnabled && displayResponseActionsOptions} @@ -231,14 +175,6 @@ const StepRuleActionsComponent: FC = ({ ) : ( <> {I18n.NO_ACTIONS_READ_PERMISSIONS} - - - - ); }, [ @@ -246,7 +182,6 @@ const StepRuleActionsComponent: FC = ({ displayActionsOptions, displayResponseActionsOptions, responseActionsEnabled, - throttleFieldComponentProps, ]); if (isReadOnlyView) { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx index 15937883fb409e..d467c3af05f8f9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx @@ -28,19 +28,3 @@ export const NO_ACTIONS_READ_PERMISSIONS = i18n.translate( 'Cannot create rule actions. You do not have "Read" permissions for the "Actions" plugin.', } ); - -export const THROTTLE_FIELD_HELP_TEXT = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText', - { - defaultMessage: - 'Select when automated actions should be performed if a rule evaluates as true.', - } -); - -export const THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery', - { - defaultMessage: - 'Select when automated actions should be performed if a rule evaluates as true. This frequency does not apply to Response Actions.', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx deleted file mode 100644 index 57dd5670dba80b..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx +++ /dev/null @@ -1,66 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useRef, useState } from 'react'; -import { getAllConnectorsUrl, getCreateConnectorUrl } from '@kbn/cases-plugin/common'; -import { convertArrayToCamelCase, KibanaServices } from '../../../../common/lib/kibana'; - -interface CaseAction { - connectorTypeId: string; - id: string; - isPreconfigured: boolean; - name: string; - referencedByCount: number; -} - -const CASE_ACTION_NAME = 'Cases'; - -export const useManageCaseAction = () => { - const hasInit = useRef(true); - const [loading, setLoading] = useState(true); - const [hasError, setHasError] = useState(false); - - useEffect(() => { - const abortCtrl = new AbortController(); - const fetchActions = async () => { - try { - const actions = convertArrayToCamelCase( - await KibanaServices.get().http.fetch(getAllConnectorsUrl(), { - method: 'GET', - signal: abortCtrl.signal, - }) - ) as CaseAction[]; - - if (!actions.some((a) => a.connectorTypeId === '.case' && a.name === CASE_ACTION_NAME)) { - await KibanaServices.get().http.post(getCreateConnectorUrl(), { - method: 'POST', - body: JSON.stringify({ - connector_type_id: '.case', - config: {}, - name: CASE_ACTION_NAME, - secrets: {}, - }), - signal: abortCtrl.signal, - }); - } - setLoading(false); - } catch { - setLoading(false); - setHasError(true); - } - }; - if (hasInit.current) { - hasInit.current = false; - fetchActions(); - } - - return () => { - abortCtrl.abort(); - }; - }, []); - return [loading, hasError]; -}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 22ba4e03dbf388..58d6e4c7c84706 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -35,6 +35,7 @@ import type { ActionsStepRule, } from './types'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; +import type { RuleAlertAction } from '../../../../../common/detection_engine/types'; describe('rule helpers', () => { moment.suppressDeprecationWarnings = true; @@ -146,7 +147,6 @@ describe('rule helpers', () => { const scheduleRuleStepData = { from: '0s', interval: '5m' }; const ruleActionsStepData = { enabled: true, - throttle: 'no_actions', actions: [], responseActions: undefined, }; @@ -410,16 +410,22 @@ describe('rule helpers', () => { describe('getActionsStepsData', () => { test('returns expected ActionsStepRule rule object', () => { + const actions: RuleAlertAction[] = [ + { + id: 'id', + group: 'group', + params: {}, + action_type_id: 'action_type_id', + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + ]; const mockedRule = { ...mockRule('test-id'), - actions: [ - { - id: 'id', - group: 'group', - params: {}, - action_type_id: 'action_type_id', - }, - ], + actions, }; const result: ActionsStepRule = getActionsStepsData(mockedRule); const expected = { @@ -429,11 +435,15 @@ describe('rule helpers', () => { group: 'group', params: {}, actionTypeId: 'action_type_id', + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, }, ], responseActions: undefined, enabled: mockedRule.enabled, - throttle: 'no_actions', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index a290ac92f6a66d..d0cfdc8b707f35 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -76,12 +76,11 @@ export const getActionsStepsData = ( response_actions?: ResponseAction[]; } ): ActionsStepRule => { - const { enabled, throttle, meta, actions = [], response_actions: responseActions } = rule; + const { enabled, meta, actions = [], response_actions: responseActions } = rule; return { actions: actions?.map(transformRuleToAlertAction), responseActions: responseActions?.map(transformRuleToAlertResponseAction), - throttle, kibanaSiemAppUrl: meta?.kibana_siem_app_url, enabled, }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index edbb3b4ecbf97d..2d2c7580ed0514 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -194,7 +194,6 @@ export interface ActionsStepRule { responseActions?: RuleResponseAction[]; enabled: boolean; kibanaSiemAppUrl?: string; - throttle?: string | null; } export interface DefineStepRuleJson { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 7df109deb6f3b0..848a0933da542e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -65,7 +65,7 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({ severity_mapping: [], updated_by: 'elastic', tags: [], - throttle: 'no_actions', + throttle: undefined, threat: getThreatMock(), exceptions_list: getListArrayMock(), filters: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts index b441d071c21c13..08a53c007dc060 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts @@ -109,8 +109,6 @@ describe('duplicateRule', () => { consumer: rule.consumer, schedule: rule.schedule, actions: rule.actions, - throttle: null, // TODO: fix? - notifyWhen: null, // TODO: fix? enabled: false, // covered in a separate test }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts index 4a99085123b410..315517504def47 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts @@ -11,6 +11,7 @@ import { ruleTypeMappings } from '@kbn/securitysolution-rules'; import type { SanitizedRule } from '@kbn/alerting-plugin/common'; import { SERVER_APP_ID } from '../../../../../../common/constants'; import type { InternalRuleCreate, RuleParams } from '../../../rule_schema'; +import { transformToActionFrequency } from '../../normalization/rule_actions'; const DUPLICATE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.cloneRule.duplicateTitle', @@ -33,6 +34,7 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise - ({ - field: 'throttle', - operation: 'set', - value: transformToAlertThrottle(throttle), - } as const); - -const getNotifyWhenOperation = (throttle: string) => - ({ - field: 'notifyWhen', - operation: 'set', - value: transformToNotifyWhen(throttle), - } as const); +import { transformToActionFrequency } from '../../normalization/rule_actions'; /** * converts bulk edit action to format of rulesClient.bulkEdit operation @@ -70,10 +55,8 @@ export const bulkEditActionToRulesClientOperation = ( { field: 'actions', operation: 'add', - value: action.value.actions, + value: transformToActionFrequency(action.value.actions, action.value.throttle), }, - getThrottleOperation(action.value.throttle), - getNotifyWhenOperation(action.value.throttle), ]; case BulkActionEditType.set_rule_actions: @@ -81,10 +64,8 @@ export const bulkEditActionToRulesClientOperation = ( { field: 'actions', operation: 'set', - value: action.value.actions, + value: transformToActionFrequency(action.value.actions, action.value.throttle), }, - getThrottleOperation(action.value.throttle), - getNotifyWhenOperation(action.value.throttle), ]; // schedule actions diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts index cfb051273255a3..6e4d573a880e0e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts @@ -123,6 +123,7 @@ describe('patchRules', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', }, group: 'default', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }), @@ -158,6 +159,7 @@ describe('patchRules', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', }, group: 'default', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts index 1996cb1652ab62..070a81692f3d5f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts @@ -12,7 +12,7 @@ import type { RuleUpdateProps } from '../../../../../../common/detection_engine/ import { transformRuleToAlertAction } from '../../../../../../common/detection_engine/transform_actions'; import type { InternalRuleUpdate, RuleParams, RuleAlertType } from '../../../rule_schema'; -import { transformToAlertThrottle, transformToNotifyWhen } from '../../normalization/rule_actions'; +import { transformToActionFrequency } from '../../normalization/rule_actions'; import { typeSpecificSnakeToCamel } from '../../normalization/rule_converters'; export interface UpdateRulesOptions { @@ -30,6 +30,9 @@ export const updateRules = async ({ return null; } + const alertActions = ruleUpdate.actions?.map(transformRuleToAlertAction) ?? []; + const actions = transformToActionFrequency(alertActions, ruleUpdate.throttle); + const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); const enabled = ruleUpdate.enabled ?? true; const newInternalRule: InternalRuleUpdate = { @@ -70,9 +73,7 @@ export const updateRules = async ({ ...typeSpecificParams, }, schedule: { interval: ruleUpdate.interval ?? '5m' }, - actions: ruleUpdate.actions != null ? ruleUpdate.actions.map(transformRuleToAlertAction) : [], - throttle: transformToAlertThrottle(ruleUpdate.throttle), - notifyWhen: transformToNotifyWhen(ruleUpdate.throttle), + actions, }; const update = await rulesClient.update({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 038dfbc9152d69..c86387f26f50a6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -131,7 +131,6 @@ describe('getExportAll', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), @@ -275,6 +274,7 @@ describe('getExportAll', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.slack', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], building_block_type: 'default', @@ -313,7 +313,6 @@ describe('getExportAll', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'rule', note: '# Investigative notes', version: 1, revision: 0, @@ -416,6 +415,7 @@ describe('getExportAll', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.email', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index de76ef3b23d8b3..35151f1b4b1d63 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -128,7 +128,6 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), @@ -284,6 +283,7 @@ describe('get_export_by_object_ids', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.slack', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], building_block_type: 'default', @@ -322,7 +322,6 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'rule', note: '# Investigative notes', version: 1, revision: 0, @@ -426,6 +425,7 @@ describe('get_export_by_object_ids', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.email', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }) @@ -508,7 +508,7 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', + throttle: undefined, note: '# Investigative notes', version: 1, revision: 0, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts index d0d5edad970f06..33fcf0bd29c204 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts @@ -5,7 +5,9 @@ * 2.0. */ +import type { SanitizedRuleAction } from '@kbn/alerting-plugin/common'; import { + NOTIFICATION_DEFAULT_FREQUENCY, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE, } from '../../../../../common/constants'; @@ -14,6 +16,7 @@ import type { RuleAlertType } from '../../rule_schema'; import { transformFromAlertThrottle, + transformToActionFrequency, transformToAlertThrottle, transformToNotifyWhen, } from './rule_actions'; @@ -151,4 +154,152 @@ describe('Rule actions normalization', () => { ).toEqual(NOTIFICATION_THROTTLE_RULE); }); }); + + describe('transformToActionFrequency', () => { + describe('actions without frequencies', () => { + const actionsWithoutFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + }, + ]; + + test.each([undefined, null, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE])( + `it sets each action's frequency attribute to default value when 'throttle' is '%s'`, + (throttle) => { + expect(transformToActionFrequency(actionsWithoutFrequencies, throttle)).toEqual( + actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })) + ); + } + ); + + test.each(['47s', '10m', '3h', '101d'])( + `it correctly transforms 'throttle = %s' and sets it as a frequency of each action`, + (throttle) => { + expect(transformToActionFrequency(actionsWithoutFrequencies, throttle)).toEqual( + actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })) + ); + } + ); + }); + + describe('actions with frequencies', () => { + const actionsWithFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + frequency: { + summary: false, + throttle: '1s', + notifyWhen: 'onThrottleInterval', + }, + }, + ]; + + test.each([ + undefined, + null, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '1h', + '1d', + ])(`it does not change actions frequency attributes when 'throttle' is '%s'`, (throttle) => { + expect(transformToActionFrequency(actionsWithFrequencies, throttle)).toEqual( + actionsWithFrequencies + ); + }); + }); + + describe('some actions with frequencies', () => { + const someActionsWithFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + frequency: { + summary: false, + throttle: '1s', + notifyWhen: 'onThrottleInterval', + }, + }, + { + group: 'group', + id: 'id-345', + actionTypeId: 'id-678', + params: {}, + }, + ]; + + test.each([undefined, null, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE])( + `it overrides each action's frequency attribute to default value when 'throttle' is '%s'`, + (throttle) => { + expect(transformToActionFrequency(someActionsWithFrequencies, throttle)).toEqual( + someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })) + ); + } + ); + + test.each(['47s', '10m', '3h', '101d'])( + `it correctly transforms 'throttle = %s' and overrides frequency attribute of each action`, + (throttle) => { + expect(transformToActionFrequency(someActionsWithFrequencies, throttle)).toEqual( + someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })) + ); + } + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts index 51dbe9d80f9864..d0e909cc5f3299 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { RuleNotifyWhenType } from '@kbn/alerting-plugin/common'; +import type { RuleActionFrequency, RuleNotifyWhenType } from '@kbn/alerting-plugin/common'; import { NOTIFICATION_THROTTLE_NO_ACTIONS, @@ -14,6 +14,38 @@ import { import type { RuleAlertType } from '../../rule_schema'; +export const transformToFrequency = (throttle: string | null | undefined): RuleActionFrequency => { + return { + summary: true, + notifyWhen: transformToNotifyWhen(throttle) ?? 'onActiveAlert', + throttle: transformToAlertThrottle(throttle), + }; +}; + +interface ActionWithFrequency { + frequency?: RuleActionFrequency; +} + +/** + * The action level `frequency` attribute should always take precedence over the rule level `throttle` + * Frequency's default value is `{ summary: true, throttle: null, notifyWhen: 'onActiveAlert' }` + * + * The transformation follows the next rules: + * - Both rule level `throttle` and all actions have `frequency` are set: we will ignore rule level `throttle` + * - Rule level `throttle` set and actions don't have `frequency` set: we will transform rule level `throttle` in action level `frequency` + * - All actions have `frequency` set: do nothing + * - Neither of them is set: we will set action level `frequency` to default value + * - Rule level `throttle` and some of the actions have `frequency` set: we will transform rule level `throttle` and set it to actions without the frequency attribute + * - Only some actions have `frequency` set and there is no rule level `throttle`: we will set default `frequency` to actions without frequency attribute + */ +export const transformToActionFrequency = ( + actions: T[], + throttle: string | null | undefined +): T[] => { + const defaultFrequency = transformToFrequency(throttle); + return actions.map((action) => ({ ...action, frequency: action.frequency ?? defaultFrequency })); +}; + /** * Given a throttle from a "security_solution" rule this will transform it into an "alerting" notifyWhen * on their saved object. diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts index 7296e90bf73e7f..66d873c2c09354 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts @@ -39,10 +39,10 @@ import { } from '../../../../../common/detection_engine/rule_schema'; import { + transformAlertToRuleAction, transformAlertToRuleResponseAction, transformRuleToAlertAction, transformRuleToAlertResponseAction, - transformAlertToRuleAction, } from '../../../../../common/detection_engine/transform_actions'; import { @@ -73,11 +73,7 @@ import type { NewTermsRuleParams, NewTermsSpecificRuleParams, } from '../../rule_schema'; -import { - transformFromAlertThrottle, - transformToAlertThrottle, - transformToNotifyWhen, -} from './rule_actions'; +import { transformFromAlertThrottle, transformToActionFrequency } from './rule_actions'; import { convertAlertSuppressionToCamel, convertAlertSuppressionToSnake } from '../utils/utils'; import { createRuleExecutionSummary } from '../../rule_monitoring'; @@ -399,6 +395,11 @@ export const convertPatchAPIToInternalSchema = ( ): InternalRuleUpdate => { const typeSpecificParams = patchTypeSpecificSnakeToCamel(nextParams, existingRule.params); const existingParams = existingRule.params; + + const alertActions = nextParams.actions?.map(transformRuleToAlertAction) ?? existingRule.actions; + const throttle = nextParams.throttle ?? transformFromAlertThrottle(existingRule); + const actions = transformToActionFrequency(alertActions, throttle); + return { name: nextParams.name ?? existingRule.name, tags: nextParams.tags ?? existingRule.tags, @@ -438,15 +439,7 @@ export const convertPatchAPIToInternalSchema = ( ...typeSpecificParams, }, schedule: { interval: nextParams.interval ?? existingRule.schedule.interval }, - actions: nextParams.actions - ? nextParams.actions.map(transformRuleToAlertAction) - : existingRule.actions, - throttle: nextParams.throttle - ? transformToAlertThrottle(nextParams.throttle) - : existingRule.throttle ?? null, - notifyWhen: nextParams.throttle - ? transformToNotifyWhen(nextParams.throttle) - : existingRule.notifyWhen ?? null, + actions, }; }; @@ -462,6 +455,10 @@ export const convertCreateAPIToInternalSchema = ( ): InternalRuleCreate => { const typeSpecificParams = typeSpecificSnakeToCamel(input); const newRuleId = input.rule_id ?? uuidv4(); + + const alertActions = input.actions?.map(transformRuleToAlertAction) ?? []; + const actions = transformToActionFrequency(alertActions, input.throttle); + return { name: input.name, tags: input.tags ?? [], @@ -502,9 +499,7 @@ export const convertCreateAPIToInternalSchema = ( }, schedule: { interval: input.interval ?? '5m' }, enabled: input.enabled ?? defaultEnabled, - actions: input.actions?.map(transformRuleToAlertAction) ?? [], - throttle: transformToAlertThrottle(input.throttle), - notifyWhen: transformToNotifyWhen(input.throttle), + actions, }; }; @@ -651,6 +646,10 @@ export const internalRuleToAPIResponse = ( const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => (obj as ResolvedSanitizedRule).outcome != null; + const alertActions = rule.actions.map(transformAlertToRuleAction); + const throttle = transformFromAlertThrottle(rule); + const actions = transformToActionFrequency(alertActions, throttle); + return { // saved object properties outcome: isResolvedRule(rule) ? rule.outcome : undefined, @@ -672,8 +671,8 @@ export const internalRuleToAPIResponse = ( // Type specific security solution rule params ...typeSpecificCamelToSnake(rule.params), // Actions - throttle: transformFromAlertThrottle(rule), - actions: rule.actions.map(transformAlertToRuleAction), + throttle: undefined, + actions, // Execution summary execution_summary: executionSummary ?? undefined, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts index bae75363ff49a2..373842fe318607 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts @@ -43,7 +43,7 @@ export const ruleOutput = (): RuleResponse => ({ tags: [], to: 'now', type: 'query', - throttle: 'no_actions', + throttle: undefined, threat: getThreatMock(), version: 1, revision: 0, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index e204aeb7bc50fe..0fbddc2a2236cb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -14,6 +14,7 @@ import { RiskScore, RiskScoreMapping, RuleActionArrayCamel, + RuleActionNotifyWhen, RuleActionThrottle, RuleIntervalFrom, RuleIntervalTo, @@ -259,13 +260,6 @@ export interface CompleteRule { ruleConfig: SanitizedRuleConfig; } -export const notifyWhen = t.union([ - t.literal('onActionGroupChange'), - t.literal('onActiveAlert'), - t.literal('onThrottleInterval'), - t.null, -]); - export const allRuleTypes = t.union([ t.literal(SIGNALS_ID), t.literal(EQL_RULE_TYPE_ID), @@ -291,7 +285,7 @@ const internalRuleCreateRequired = t.type({ }); const internalRuleCreateOptional = t.partial({ throttle: t.union([RuleActionThrottle, t.null]), - notifyWhen, + notifyWhen: t.union([RuleActionNotifyWhen, t.null]), }); export const internalRuleCreate = t.intersection([ internalRuleCreateOptional, @@ -310,7 +304,7 @@ const internalRuleUpdateRequired = t.type({ }); const internalRuleUpdateOptional = t.partial({ throttle: t.union([RuleActionThrottle, t.null]), - notifyWhen, + notifyWhen: t.union([RuleActionNotifyWhen, t.null]), }); export const internalRuleUpdate = t.intersection([ internalRuleUpdateOptional, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d3c4ae39380771..290368a66b4ed9 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28999,9 +28999,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "Sélectionner des index de menaces", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "Au minimum un modèle d'indexation est requis.", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "Tous les résultats", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText": "Sélectionnez le moment auquel les actions automatiques doivent être effectuées si une règle est évaluée comme vraie.", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery": "Sélectionnez le moment auquel les actions automatiques doivent être effectuées si une règle est évaluée comme vraie. Cette fréquence ne s'applique pas aux actions de réponse.", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel": "Fréquence des actions", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noReadActionsPrivileges": "Impossible de créer des actions de règle. Vous ne disposez pas des autorisations \"Lire\" pour le plug-in \"Actions\".", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithEnablingTitle": "Créer et activer la règle", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutEnablingTitle": "Créer la règle sans l’activer", @@ -29959,12 +29956,9 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.tooltip": " Si vous dupliquez les exceptions, la liste des exceptions partagée sera dupliquée par référence et l'exception de la règle par défaut sera copiée et créée comme une nouvelle exception", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastTitle": "Règles dupliquées", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle": "Dupliquer", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.actionFrequencyDetail": "La fréquence des actions que vous sélectionnez ci-dessous est appliquée à toutes les actions (nouvelles et existantes) pour toutes les règles sélectionnées.", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.formTitle": "Ajouter des actions sur les règles", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.overwriteCheckboxLabel": "Écraser toutes les actions sur les règles sélectionnées", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.ruleVariablesDetail": "Les variables de règle peuvent affecter uniquement certaines règles sélectionnées, en fonction des types de règle (par exemple, \\u007b\\u007bcontext.rule.threshold\\u007d\\u007d affichera uniquement les valeurs des règles de seuil).", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleHelpText": "Sélectionnez le moment auquel les actions automatiques doivent être effectuées si une règle est évaluée comme vraie.", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleLabel": "Fréquence des actions", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage.buttonLabel": "Enregistrer", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.formTitle": "Appliquer le modèle de chronologie", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.templateSelectorDefaultValue": "Aucun", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 060f5364f947f5..c1c8d3f14fe4e9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -28978,9 +28978,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "脅威インデックスを選択", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "インデックスパターンが最低1つ必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "すべての結果", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText": "ルールが true であると評価された場合に自動アクションを実行するタイミングを選択します。", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery": "ルールが true であると評価された場合に自動アクションを実行するタイミングを選択します。この頻度は対応アクションには適用されません。", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel": "アクション頻度", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noReadActionsPrivileges": "ルールアクションを作成できません。「Actions」プラグインの「読み取り」アクセス権がありません。", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithEnablingTitle": "ルールを作成して有効にする", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutEnablingTitle": "有効にせずにルールを作成", @@ -29938,12 +29935,9 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.tooltip": " 例外を複製する場合は、参照によって共有例外リストが複製されます。それから、デフォルトルール例外がコピーされ、新しい例外が作成されます", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastTitle": "ルールが複製されました", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle": "複製", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.actionFrequencyDetail": "以下で選択したアクション頻度は、すべての選択したルールのすべてのアクション(新規と既存のアクション)に適用されます。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.formTitle": "ルールアクションを追加", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.overwriteCheckboxLabel": "すべての選択したルールアクションを上書き", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.ruleVariablesDetail": "ルールタイプによっては、ルール変数が選択舌一部のルールにのみ影響する場合があります(例:\\u007b\\u007bcontext.rule.threshold\\u007d\\u007dはしきい値ルールの値のみを表示します)。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleHelpText": "ルールが true であると評価された場合に自動アクションを実行するタイミングを選択します。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleLabel": "アクション頻度", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage.buttonLabel": "保存", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.formTitle": "タイムラインテンプレートを適用", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.templateSelectorDefaultValue": "なし", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e577ba4753e8f0..40989bc638cc50 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -28994,9 +28994,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchingIcesHelperDescription": "选择威胁索引", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.threatMatchoutputIndiceNameFieldRequiredError": "至少需要一种索引模式。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.thresholdField.thresholdFieldPlaceholderText": "所有结果", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText": "选择在规则评估为 true 时应执行自动操作的时间。", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery": "选择在规则评估为 true 时应执行自动操作的时间。此频率不适用于响应操作。", - "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel": "操作频率", "xpack.securitySolution.detectionEngine.createRule.stepRuleActions.noReadActionsPrivileges": "无法创建规则操作。您对“操作”插件没有“读”权限。", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithEnablingTitle": "创建并启用规则", "xpack.securitySolution.detectionEngine.createRule.stepScheduleRule.completeWithoutEnablingTitle": "创建规则但不启用", @@ -29954,12 +29951,9 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.tooltip": " 如果您复制例外,则会通过引用复制共享例外列表,然后复制默认规则例外,并将其创建为新例外", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastTitle": "规则已复制", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle": "复制", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.actionFrequencyDetail": "您在下面选择的操作频率将应用于所有选定规则的所有操作(新操作和现有操作)。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.formTitle": "添加规则操作", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.overwriteCheckboxLabel": "覆盖所有选定规则操作", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.ruleVariablesDetail": "基于规则类型,规则变量可能仅影响您选择的某些规则(例如,\\u007b\\u007bcontext.rule.threshold\\u007d\\u007d 将仅显示阈值规则的值)。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleHelpText": "选择在规则评估为 true 时应执行自动操作的时间。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.throttleLabel": "操作频率", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage.buttonLabel": "保存", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.formTitle": "应用时间线模板", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.templateSelectorDefaultValue": "无", diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 62fa4d3786db68..2c6ae644d911cc 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -99,7 +99,6 @@ export default ({ getService }: FtrProviderContext) => { to: 'now', type: 'query', threat: [], - throttle: 'no_actions', exceptions_list: [], version: 1, revision: 0, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts index 4346087684fd49..e6adaa069b4974 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts @@ -7,7 +7,12 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; import { RuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; @@ -32,8 +37,15 @@ import { waitForSignalsToBePresent, getThresholdRuleForSignalTesting, waitForRulePartialFailure, + createRule, } from '../../utils'; import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -200,7 +212,6 @@ export default ({ getService }: FtrProviderContext) => { to: 'now', type: 'query', threat: [], - throttle: 'no_actions', exceptions_list: [], version: 1, }; @@ -566,5 +577,139 @@ export default ({ getService }: FtrProviderContext) => { expect(rule?.execution_summary?.last_execution.status).to.eql('partial failure'); }); }); + + describe('per-action frequencies', () => { + const createSingleRule = async (rule: RuleCreateProps) => { + const createdRule = await createRule(supertest, log, rule); + createdRule.actions = removeUUIDFromActions(createdRule.actions); + return createdRule; + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithFrequencies; + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts index 6d0e79975bfd68..63d7e18367dc6e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts @@ -10,7 +10,11 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_RULES_BULK_CREATE, DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, } from '@kbn/security-solution-plugin/common/constants'; +import { RuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -27,6 +31,12 @@ import { removeServerGeneratedPropertiesIncludingRuleId, waitForRuleSuccess, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -276,6 +286,141 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + describe('per-action frequencies', () => { + const bulkCreateSingleRule = async (rule: RuleCreateProps) => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_CREATE) + .set('kbn-xsrf', 'true') + .send([rule]) + .expect(200); + + const createdRule = body[0]; + createdRule.actions = removeUUIDFromActions(createdRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(createdRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(createdRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithFrequencies; + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(createdRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts index a394bef16cd22b..e029c13aff8e5b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts @@ -176,6 +176,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts index fa83ceb9a19b1c..8e0ad07a782c31 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts @@ -309,6 +309,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -357,6 +358,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); expect(body[1].actions).to.eql([ @@ -368,6 +370,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts index c9cb430e144ba5..72fb3631ac0c35 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts @@ -8,6 +8,7 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { RuleResponse } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { binaryToString, @@ -208,10 +209,17 @@ export default ({ getService }: FtrProviderContext): void => { const outputRule1: ReturnType = { ...getSimpleRuleOutput('rule-1'), actions: [ - { ...action1, uuid: firstRule.actions[0].uuid }, - { ...action2, uuid: firstRule.actions[1].uuid }, + { + ...action1, + uuid: firstRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + ...action2, + uuid: firstRule.actions[1].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, ], - throttle: 'rule', }; expect(firstRule).toEqual(outputRule1); }); @@ -258,13 +266,23 @@ export default ({ getService }: FtrProviderContext): void => { const outputRule1: ReturnType = { ...getSimpleRuleOutput('rule-2'), - actions: [{ ...action, uuid: firstRule.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: firstRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; const outputRule2: ReturnType = { ...getSimpleRuleOutput('rule-1'), - actions: [{ ...action, uuid: secondRule.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: secondRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; expect(firstRule).toEqual(outputRule1); expect(secondRule).toEqual(outputRule2); @@ -437,9 +455,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const firstRule = removeServerGeneratedProperties(firstRuleParsed); @@ -514,6 +532,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -523,9 +542,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const firstRule = removeServerGeneratedProperties(firstRuleParsed); @@ -631,6 +650,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -640,9 +660,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const outputRule2: ReturnType = { @@ -656,6 +676,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -665,9 +686,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); @@ -682,7 +703,8 @@ export default ({ getService }: FtrProviderContext): void => { }); }; -function expectToMatchRuleSchema(obj: unknown): void { +function expectToMatchRuleSchema(obj: RuleResponse): void { + expect(obj.throttle).toBeUndefined(); expect(obj).toEqual({ id: expect.any(String), rule_id: expect.any(String), @@ -718,7 +740,6 @@ function expectToMatchRuleSchema(obj: unknown): void { language: expect.any(String), index: expect.arrayContaining([]), query: expect.any(String), - throttle: expect.any(String), actions: expect.arrayContaining([]), }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts index 7cd260697be721..348400580edd26 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts @@ -126,8 +126,13 @@ export default ({ getService }: FtrProviderContext): void => { const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: body.data[0].actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: body.data[0].actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; body.data = [removeServerGeneratedProperties(body.data[0])]; @@ -171,8 +176,13 @@ export default ({ getService }: FtrProviderContext): void => { const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: body.data[0].actions[0].uuid }], - throttle: '1h', // <-- throttle makes this a scheduled action + actions: [ + { + ...action, + uuid: body.data[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, + }, + ], }; body.data = [removeServerGeneratedProperties(body.data[0])]; @@ -239,9 +249,9 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: hookAction.actionTypeId, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; body.data = [removeServerGeneratedProperties(body.data[0])]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts index 6ba7871b1dbf5b..9fd2542664f3b9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts @@ -75,8 +75,8 @@ export default ({ getService }: FtrProviderContext) => { expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); expect(ruleSO?.alert.actions).to.eql([]); - expect(ruleSO?.alert.throttle).to.eql('no_actions'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql(null); }); it('migrates legacy actions for rule with action run on every run', async () => { @@ -122,6 +122,7 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, { actionRef: 'action_1', @@ -133,10 +134,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[1].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('rule'); - expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert'); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -197,6 +199,7 @@ export default ({ getService }: FtrProviderContext) => { actionRef: 'action_0', group: 'default', uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { actionTypeId: '.slack', @@ -206,10 +209,11 @@ export default ({ getService }: FtrProviderContext) => { actionRef: 'action_1', group: 'default', uuid: ruleSO?.alert.actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('1h'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -269,10 +273,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('1d'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -327,10 +332,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '7d', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('7d'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts index 98b64726bbaca1..ae434a50df98f0 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts @@ -7,7 +7,13 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -24,7 +30,14 @@ import { getSimpleMlRule, createLegacyRuleAction, getLegacyActionSO, + getSimpleRuleWithoutRuleId, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -377,9 +390,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; - outputRule.throttle = '1h'; outputRule.revision = 1; expect(bodyToCompare).to.eql(outputRule); @@ -414,5 +427,168 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + describe('patch per-action frequencies', () => { + const patchSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + const { body: patchedRule } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleId, throttle, actions }) + .expect(200); + + patchedRule.actions = removeUUIDFromActions(patchedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(patchedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts index 1a26b17bca42ee..2d80d17f9100ea 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts @@ -207,9 +207,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; - outputRule.throttle = '1h'; outputRule.revision = 1; expect(bodyToCompare).to.eql(outputRule); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index e23db2120eb7e5..185d8203514597 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_RULES_BULK_ACTION, DETECTION_ENGINE_RULES_URL, - NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE, } from '@kbn/security-solution-plugin/common/constants'; import type { RuleResponse } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; @@ -172,7 +171,6 @@ export default ({ getService }: FtrProviderContext): void => { const rule = removeServerGeneratedProperties(JSON.parse(ruleJson)); expect(rule).to.eql({ ...getSimpleRuleOutput(), - throttle: 'rule', actions: [ { action_type_id: '.webhook', @@ -182,6 +180,7 @@ export default ({ getService }: FtrProviderContext): void => { body: '{"test":"a default action"}', }, uuid: rule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }); @@ -335,6 +334,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: ruleBody.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); // we want to ensure rule is executing successfully, to prevent any AAD issues related to partial update of rule SO @@ -407,6 +407,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: ruleBody.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -705,6 +706,7 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, ...(uuid ? { uuid } : {}), + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -1334,6 +1336,7 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: setTagsRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -1618,6 +1621,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1675,6 +1679,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1786,6 +1791,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1838,6 +1844,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1892,12 +1899,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const expectedRuleActions = [ - { ...defaultRuleAction, uuid: body.attributes.results.updated[0].actions[0].uuid }, + { + ...defaultRuleAction, + uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, { ...webHookActionMock, id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1960,12 +1972,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const expectedRuleActions = [ - { ...defaultRuleAction, uuid: body.attributes.results.updated[0].actions[0].uuid }, + { + ...defaultRuleAction, + uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, { ...slackConnectorMockProps, id: slackConnector.id, action_type_id: '.slack', uuid: body.attributes.results.updated[0].actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -2014,20 +2031,22 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql([ - { ...defaultRuleAction, uuid: createdRule.actions[0].uuid }, - ]); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); expect(readRule.actions).to.eql([ - { ...defaultRuleAction, uuid: createdRule.actions[0].uuid }, + { + ...defaultRuleAction, + uuid: createdRule.actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, ]); }); - it('should change throttle if actions list in payload is empty', async () => { + it('should not change throttle if actions list in payload is empty', async () => { // create a new connector const webHookConnector = await createWebHookConnector(); @@ -2063,13 +2082,14 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].throttle).to.be('1h'); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.throttle).to.eql('1h'); + expect(readRule.throttle).to.eql(undefined); + expect(readRule.actions).to.eql(createdRule.actions); }); }); @@ -2117,6 +2137,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: editedRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); // version of prebuilt rule should not change @@ -2131,6 +2152,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: readRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); expect(prebuiltRule.version).to.be(readRule.version); @@ -2207,7 +2229,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ]; casesForEmptyActions.forEach(({ payloadThrottle }) => { - it(`throttle is set to NOTIFICATION_THROTTLE_NO_ACTIONS, if payload throttle="${payloadThrottle}" and actions list is empty`, async () => { + it(`should not update throttle, if payload throttle="${payloadThrottle}" and actions list is empty`, async () => { const ruleId = 'ruleId'; const createdRule = await createRule(supertest, log, { ...getSimpleRule(ruleId), @@ -2230,34 +2252,32 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].throttle).to.eql( - NOTIFICATION_THROTTLE_NO_ACTIONS - ); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); - expect(rule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(rule.throttle).to.eql(undefined); }); }); const casesForNonEmptyActions = [ { payloadThrottle: NOTIFICATION_THROTTLE_RULE, - expectedThrottle: NOTIFICATION_THROTTLE_RULE, + expectedThrottle: undefined, }, { payloadThrottle: '1h', - expectedThrottle: '1h', + expectedThrottle: undefined, }, { payloadThrottle: '1d', - expectedThrottle: '1d', + expectedThrottle: undefined, }, { payloadThrottle: '7d', - expectedThrottle: '7d', + expectedThrottle: undefined, }, ]; [BulkActionEditType.set_rule_actions, BulkActionEditType.add_rule_actions].forEach( @@ -2295,10 +2315,23 @@ export default ({ getService }: FtrProviderContext): void => { // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].throttle).to.eql(expectedThrottle); + const expectedActions = body.attributes.results.updated[0].actions.map( + (action: any) => ({ + ...action, + frequency: { + summary: true, + throttle: payloadThrottle !== 'rule' ? payloadThrottle : null, + notifyWhen: + payloadThrottle !== 'rule' ? 'onThrottleInterval' : 'onActiveAlert', + }, + }) + ); + // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); expect(rule.throttle).to.eql(expectedThrottle); + expect(rule.actions).to.eql(expectedActions); }); }); } @@ -2311,11 +2344,11 @@ export default ({ getService }: FtrProviderContext): void => { const cases = [ { payload: { throttle: '1d' }, - expected: { notifyWhen: 'onThrottleInterval' }, + expected: { notifyWhen: null }, }, { payload: { throttle: NOTIFICATION_THROTTLE_RULE }, - expected: { notifyWhen: 'onActiveAlert' }, + expected: { notifyWhen: null }, }, ]; cases.forEach(({ payload, expected }) => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts index d2162e02d443ea..31904342e294f2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts @@ -135,8 +135,13 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare = removeServerGeneratedProperties(body); const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: bodyToCompare.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; expect(bodyToCompare).to.eql(ruleWithActions); }); @@ -174,8 +179,13 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare = removeServerGeneratedProperties(body); const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: bodyToCompare.actions[0].uuid }], - throttle: '1h', // <-- throttle makes this a scheduled action + actions: [ + { + ...action, + uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, + }, + ], }; expect(bodyToCompare).to.eql(ruleWithActions); }); @@ -236,9 +246,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: hookAction.actionTypeId, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; expect(bodyToCompare).to.eql(ruleWithActions); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts index eedfaee3ec531c..4b218d07b76130 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts @@ -56,7 +56,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('creating a rule', () => { - it('When creating a new action and attaching it to a rule, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating a new action and attaching it to a rule, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -66,10 +66,16 @@ export default ({ getService }: FtrProviderContext) => { const rule = await createRule(supertest, log, getRuleWithWebHookAction(hookAction.id)); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: null, + notify_when: 'onActiveAlert', + }); + expect(notifyWhen).to.eql(null); }); it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { @@ -105,7 +111,7 @@ export default ({ getService }: FtrProviderContext) => { expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { const ruleWithThrottle: RuleCreateProps = { ...getSimpleRule(), throttle: NOTIFICATION_THROTTLE_RULE, @@ -115,7 +121,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -125,10 +131,10 @@ export default ({ getService }: FtrProviderContext) => { throttle: NOTIFICATION_THROTTLE_RULE, }; const rule = await createRule(supertest, log, ruleWithThrottle); - expect(rule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(rule.throttle).to.eql(undefined); }); - it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -142,13 +148,19 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: null, + notify_when: 'onActiveAlert', + }); + expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "1h" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onThrottleInterval"', async () => { + it('When creating throttle with "1h" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { const ruleWithThrottle: RuleCreateProps = { ...getSimpleRule(), throttle: '1h', @@ -158,10 +170,10 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onThrottleInterval'); + expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "1h" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onThrottleInterval"', async () => { + it('When creating throttle with "1h" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -175,10 +187,16 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onThrottleInterval'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: '1h', + notify_when: 'onThrottleInterval', + }); + expect(notifyWhen).to.eql(null); }); }); @@ -193,7 +211,7 @@ export default ({ getService }: FtrProviderContext) => { const rule = await createRule(supertest, log, getRuleWithWebHookAction(hookAction.id)); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(readRule.throttle).to.eql(undefined); }); it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, we should return "NOTIFICATION_THROTTLE_NO_ACTIONS" when doing a read', async () => { @@ -203,7 +221,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -214,7 +232,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); it('When creating a new action and attaching it to a rule, if we change the alert to a "muteAll" through the kibana alerting API, we should get back "NOTIFICATION_THROTTLE_NO_ACTIONS" ', async () => { @@ -232,7 +250,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(204); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); }); @@ -249,7 +267,7 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, ruleWithWebHookAction); ruleWithWebHookAction.name = 'some other name'; const updated = await updateRule(supertest, log, ruleWithWebHookAction); - expect(updated.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(updated.throttle).to.eql(undefined); }); it('will not change the "muteAll" or "notifyWhen" if we update some part of the rule', async () => { @@ -268,7 +286,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${updated.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -284,7 +302,7 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, ruleWithWebHookAction); ruleWithWebHookAction.actions = []; const updated = await updateRule(supertest, log, ruleWithWebHookAction); - expect(updated.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(updated.throttle).to.eql(undefined); }); }); @@ -306,7 +324,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ rule_id: rule.rule_id, name: 'some other name' }) .expect(200); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(readRule.throttle).to.eql(undefined); }); it('will not change the "muteAll" or "notifyWhen" if we patch part of the rule', async () => { @@ -329,7 +347,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -350,7 +368,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ rule_id: rule.rule_id, actions: [] }) .expect(200); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index 8396f72a9bb083..82b23e8b381d1e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -7,7 +7,13 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -28,7 +34,14 @@ import { createLegacyRuleAction, getThresholdRuleForSignalTesting, getLegacyActionSO, + getSimpleRuleWithoutRuleId, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -185,8 +198,6 @@ export default ({ getService }: FtrProviderContext) => { outputRule.revision = 1; // Expect an empty array outputRule.actions = []; - // Expect "no_actions" - outputRule.throttle = 'no_actions'; const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); expect(bodyToCompare).to.eql(outputRule); }); @@ -221,7 +232,7 @@ export default ({ getService }: FtrProviderContext) => { id: connector.body.id, action_type_id: connector.body.connector_type_id, params: { - message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, }; // update a simple rule's name @@ -229,7 +240,6 @@ export default ({ getService }: FtrProviderContext) => { updatedRule.rule_id = createRuleBody.rule_id; updatedRule.name = 'some other name'; updatedRule.actions = [action1]; - updatedRule.throttle = '1d'; delete updatedRule.id; const { body } = await supertest @@ -249,13 +259,12 @@ export default ({ getService }: FtrProviderContext) => { group: 'default', id: connector.body.id, params: { - message: - 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions![0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]; - outputRule.throttle = '1d'; expect(bodyToCompare).to.eql(outputRule); @@ -662,6 +671,176 @@ export default ({ getService }: FtrProviderContext) => { expect(outputRule.saved_id).to.be(undefined); }); }); + + describe('per-action frequencies', () => { + const updateSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + // update a simple rule's `throttle` and `actions` + const ruleToUpdate = getSimpleRuleUpdate(); + ruleToUpdate.throttle = throttle; + ruleToUpdate.actions = actions; + ruleToUpdate.id = ruleId; + delete ruleToUpdate.rule_id; + + const { body: updatedRule } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleToUpdate) + .expect(200); + + updatedRule.actions = removeUUIDFromActions(updatedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(updatedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts index 0c8dcacd0ebbf1..5da2e8f1c10cd5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts @@ -11,8 +11,12 @@ import { RuleResponse } from '@kbn/security-solution-plugin/common/detection_eng import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_RULES_BULK_UPDATE, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + NOTIFICATION_DEFAULT_FREQUENCY, } from '@kbn/security-solution-plugin/common/constants'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -25,7 +29,16 @@ import { getSimpleRule, createLegacyRuleAction, getLegacyActionSO, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleWithoutRuleId, + getSimpleRuleOutputWithoutRuleId, } from '../../utils'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -138,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => { id: connector.body.id, action_type_id: connector.body.connector_type_id, params: { - message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, }; const [rule1, rule2] = await Promise.all([ @@ -160,12 +173,10 @@ export default ({ getService }: FtrProviderContext) => { const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.name = 'some other name'; updatedRule1.actions = [action1]; - updatedRule1.throttle = '1d'; const updatedRule2 = getSimpleRuleUpdate('rule-2'); updatedRule2.name = 'some other name'; updatedRule2.actions = [action1]; - updatedRule2.throttle = '1d'; // update both rule names const { body }: { body: RuleResponse[] } = await supertest @@ -189,13 +200,12 @@ export default ({ getService }: FtrProviderContext) => { group: 'default', id: connector.body.id, params: { - message: - 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]; - outputRule.throttle = '1d'; expect(bodyToCompare).to.eql(outputRule); }); @@ -247,7 +257,6 @@ export default ({ getService }: FtrProviderContext) => { outputRule.name = 'some other name'; outputRule.revision = 1; outputRule.actions = []; - outputRule.throttle = 'no_actions'; const bodyToCompare = removeServerGeneratedProperties(response); expect(bodyToCompare).to.eql(outputRule); }); @@ -603,5 +612,177 @@ export default ({ getService }: FtrProviderContext) => { ]); }); }); + + describe('bulk per-action frequencies', () => { + const bulkUpdateSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + // update a simple rule's `throttle` and `actions` + const ruleToUpdate = getSimpleRuleUpdate(); + ruleToUpdate.throttle = throttle; + ruleToUpdate.actions = actions; + ruleToUpdate.id = ruleId; + delete ruleToUpdate.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_BULK_UPDATE) + .set('kbn-xsrf', 'true') + .send([ruleToUpdate]) + .expect(200); + + const updatedRule = body[0]; + updatedRule.actions = removeUUIDFromActions(updatedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(updatedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts index af661230143835..31b8c4571e2f5f 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts @@ -92,7 +92,6 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial = 'http://www.example.com/some-article-about-attack', 'Some plain text string here explaining why this is a valid thing to look out for', ], - throttle: 'no_actions', timeline_id: 'timeline_id', timeline_title: 'timeline_title', updated_by: 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts b/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts new file mode 100644 index 00000000000000..519cd9136c6046 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; + +import { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; +import { getSlackAction } from './get_slack_action'; +import { getWebHookAction } from './get_web_hook_action'; + +const createConnector = async ( + supertest: SuperTest.SuperTest, + payload: Record +) => + (await supertest.post('/api/actions/action').set('kbn-xsrf', 'true').send(payload).expect(200)) + .body; +const createWebHookConnector = (supertest: SuperTest.SuperTest) => + createConnector(supertest, getWebHookAction()); +const createSlackConnector = (supertest: SuperTest.SuperTest) => + createConnector(supertest, getSlackAction()); + +export const getActionsWithoutFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + }, + ]; +}; + +export const getActionsWithFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + frequency: { summary: false, throttle: '3d', notifyWhen: 'onThrottleInterval' }, + }, + ]; +}; + +export const getSomeActionsWithFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + frequency: { summary: false, throttle: '3d', notifyWhen: 'onThrottleInterval' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + }, + ]; +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts index 9826d9a2b98b45..cdadcce39e0a83 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts @@ -40,7 +40,7 @@ export const getMockSharedResponseSchema = ( tags: [], to: 'now', threat: [], - throttle: 'no_actions', + throttle: undefined, exceptions_list: [], version: 1, revision: 0, diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts index 25776fefd56875..7ecee679e50b3e 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { NOTIFICATION_DEFAULT_FREQUENCY } from '@kbn/security-solution-plugin/common/constants'; import { getSimpleRuleOutput } from './get_simple_rule_output'; import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties'; @@ -13,7 +14,6 @@ export const getSimpleRuleOutputWithWebHookAction = ( uuid: string ): RuleWithoutServerGeneratedProperties => ({ ...getSimpleRuleOutput(), - throttle: 'rule', actions: [ { action_type_id: '.webhook', @@ -23,6 +23,7 @@ export const getSimpleRuleOutputWithWebHookAction = ( body: '{}', }, uuid, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, }, ], }); diff --git a/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts b/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts new file mode 100644 index 00000000000000..08d95bc7502125 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; + +export const removeUUIDFromActions = (actions: RuleActionArray): RuleActionArray => { + return actions.map(({ uuid, ...restOfAction }) => ({ + ...restOfAction, + })); +}; From 158ea8546829cbeac3246c75762901382c0ac232 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Sun, 23 Apr 2023 13:01:19 -0400 Subject: [PATCH 25/65] feat(slo): Synchronize cursor on SLO charts (#155560) --- .../pages/slo_details/components/wide_chart.tsx | 16 ++++++++++++++-- .../pages/slo_details/slo_details.test.tsx | 2 +- .../utils/kibana_react.storybook_decorator.tsx | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/wide_chart.tsx b/x-pack/plugins/observability/public/pages/slo_details/components/wide_chart.tsx index 7fc212dbd42756..dbbacc6b1be914 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/components/wide_chart.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/components/wide_chart.tsx @@ -15,10 +15,11 @@ import { ScaleType, Settings, } from '@elastic/charts'; -import React from 'react'; +import React, { useRef } from 'react'; import { EuiIcon, EuiLoadingChart, useEuiTheme } from '@elastic/eui'; import numeral from '@elastic/numeral'; import moment from 'moment'; +import { useActiveCursor } from '@kbn/charts-plugin/public'; import { ChartData } from '../../../typings'; import { useKibana } from '../../../utils/kibana_react'; @@ -45,18 +46,29 @@ export function WideChart({ chart, data, id, isLoading, state }: Props) { const color = state === 'error' ? euiTheme.colors.danger : euiTheme.colors.success; const ChartComponent = chart === 'area' ? AreaSeries : LineSeries; + const chartRef = useRef(null); + const handleCursorUpdate = useActiveCursor(charts.activeCursor, chartRef, { + isDateHistogram: true, + }); + if (isLoading) { return ; } return ( - + } + onPointerUpdate={handleCursorUpdate} + externalPointerEvents={{ + tooltip: { visible: true }, + }} + pointerUpdateDebounce={0} + pointerUpdateTrigger={'x'} /> { useKibanaMock.mockReturnValue({ services: { application: { navigateToUrl: mockNavigate }, - charts: chartPluginMock.createSetupContract(), + charts: chartPluginMock.createStartContract(), http: { basePath: { prepend: mockBasePathPrepend, diff --git a/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx index 5a562958cb2998..a0e5a1f0e037b8 100644 --- a/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx @@ -65,6 +65,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { useChartsBaseTheme: () => {}, useChartsTheme: () => {}, }, + activeCursor: () => {}, }, data: {}, dataViews: { From 9c3111e57f9d046012bf66222dc71c40d3afbd8a Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Sun, 23 Apr 2023 13:02:46 -0400 Subject: [PATCH 26/65] chore(slo): change no rule badge style (#155546) --- .../slos/components/badges/slo_rules_badge.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx index c60617152cb003..2b80e8852bdae0 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; -import { EuiBadge, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; + import { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo'; export interface Props { @@ -26,16 +27,9 @@ export function SloRulesBadge({ rules, onClick }: Props) { })} display="block" > - - - + + + ); } From 50bb555e7af7b6a9b73799461d3278fb7d42352f Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Sun, 23 Apr 2023 13:04:12 -0400 Subject: [PATCH 27/65] feat(slo): add feedback button (#155468) --- .../slo/feedback_button/feedback_button.tsx | 28 +++++++++++++++++++ .../public/pages/slo_details/slo_details.tsx | 2 ++ .../public/pages/slo_edit/slo_edit.tsx | 3 +- .../observability/public/pages/slos/slos.tsx | 4 ++- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx diff --git a/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx b/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx new file mode 100644 index 00000000000000..f72e4b008f390f --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +const SLO_FEEDBACK_LINK = 'https://ela.st/slo-feedback'; + +export function FeedbackButton() { + return ( + + {i18n.translate('xpack.observability.slo.feedbackButtonLabel', { + defaultMessage: 'Tell us what you think!', + })} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx index bec6e6608abccf..48401ca29f94e3 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx @@ -26,6 +26,7 @@ import { paths } from '../../config/paths'; import type { SloDetailsPathParams } from './types'; import type { ObservabilityAppServices } from '../../application/types'; import { AutoRefreshButton } from '../slos/components/auto_refresh_button'; +import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button'; export function SloDetailsPage() { const { @@ -69,6 +70,7 @@ export function SloDetailsPage() { isAutoRefreshing={isAutoRefreshing} onClick={handleToggleAutoRefresh} />, + , ], bottomBorder: false, }} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx index 57e27810815d6c..ede2d15d3f23dc 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx @@ -16,6 +16,7 @@ import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; import { useLicense } from '../../hooks/use_license'; import { SloEditForm } from './components/slo_edit_form'; +import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button'; export function SloEditPage() { const { @@ -58,7 +59,7 @@ export function SloEditPage() { : i18n.translate('xpack.observability.sloCreatePageTitle', { defaultMessage: 'Create new SLO', }), - rightSideItems: [], + rightSideItems: [], bottomBorder: false, }} data-test-subj="slosEditPage" diff --git a/x-pack/plugins/observability/public/pages/slos/slos.tsx b/x-pack/plugins/observability/public/pages/slos/slos.tsx index e224158ef9c2aa..b81998d8452de7 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.tsx @@ -21,6 +21,7 @@ import { AutoRefreshButton } from './components/auto_refresh_button'; import { paths } from '../../config/paths'; import type { ObservabilityAppServices } from '../../application/types'; import { HeaderTitle } from './components/header_title'; +import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button'; export function SlosPage() { const { @@ -69,7 +70,7 @@ export function SlosPage() { rightSideItems: [ , + , ], bottomBorder: false, }} From 743325dc85c39209131112575accde14577bae36 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 24 Apr 2023 01:02:05 -0400 Subject: [PATCH 28/65] [api-docs] 2023-04-24 Daily api_docs build (#155584) Generated by https://buildkite.com/elastic/kibana-api-docs-daily/builds/317 --- api_docs/actions.mdx | 2 +- api_docs/advanced_settings.mdx | 2 +- api_docs/aiops.mdx | 2 +- api_docs/alerting.devdocs.json | 71 +- api_docs/alerting.mdx | 4 +- api_docs/apm.devdocs.json | 6 + api_docs/apm.mdx | 2 +- api_docs/asset_manager.mdx | 2 +- api_docs/banners.mdx | 2 +- api_docs/bfetch.mdx | 2 +- api_docs/canvas.mdx | 2 +- api_docs/cases.mdx | 2 +- api_docs/charts.mdx | 2 +- api_docs/cloud.mdx | 2 +- api_docs/cloud_chat.mdx | 2 +- api_docs/cloud_data_migration.mdx | 2 +- api_docs/cloud_defend.mdx | 2 +- api_docs/cloud_experiments.mdx | 2 +- api_docs/cloud_security_posture.mdx | 2 +- api_docs/console.mdx | 2 +- api_docs/content_management.devdocs.json | 140 ++- api_docs/content_management.mdx | 4 +- api_docs/controls.mdx | 2 +- api_docs/custom_integrations.mdx | 2 +- api_docs/dashboard.mdx | 2 +- api_docs/dashboard_enhanced.mdx | 2 +- api_docs/data.mdx | 2 +- api_docs/data_query.mdx | 2 +- api_docs/data_search.mdx | 2 +- api_docs/data_view_editor.mdx | 2 +- api_docs/data_view_field_editor.mdx | 2 +- api_docs/data_view_management.mdx | 2 +- api_docs/data_views.mdx | 2 +- api_docs/data_visualizer.mdx | 2 +- api_docs/deprecations_by_api.mdx | 2 +- api_docs/deprecations_by_plugin.mdx | 2 +- api_docs/deprecations_by_team.mdx | 2 +- api_docs/dev_tools.mdx | 2 +- api_docs/discover.mdx | 2 +- api_docs/discover_enhanced.mdx | 2 +- api_docs/ecs_data_quality_dashboard.mdx | 2 +- api_docs/embeddable.mdx | 2 +- api_docs/embeddable_enhanced.mdx | 2 +- api_docs/encrypted_saved_objects.mdx | 2 +- api_docs/enterprise_search.mdx | 2 +- api_docs/es_ui_shared.mdx | 2 +- api_docs/event_annotation.mdx | 2 +- api_docs/event_log.devdocs.json | 6 +- api_docs/event_log.mdx | 2 +- api_docs/exploratory_view.mdx | 2 +- api_docs/expression_error.mdx | 2 +- api_docs/expression_gauge.mdx | 2 +- api_docs/expression_heatmap.mdx | 2 +- api_docs/expression_image.mdx | 2 +- api_docs/expression_legacy_metric_vis.mdx | 2 +- api_docs/expression_metric.mdx | 2 +- api_docs/expression_metric_vis.mdx | 2 +- api_docs/expression_partition_vis.mdx | 2 +- api_docs/expression_repeat_image.mdx | 2 +- api_docs/expression_reveal_image.mdx | 2 +- api_docs/expression_shape.mdx | 2 +- api_docs/expression_tagcloud.mdx | 2 +- api_docs/expression_x_y.mdx | 2 +- api_docs/expressions.mdx | 2 +- api_docs/features.mdx | 2 +- api_docs/field_formats.mdx | 2 +- api_docs/file_upload.mdx | 2 +- api_docs/files.mdx | 2 +- api_docs/files_management.mdx | 2 +- api_docs/fleet.mdx | 2 +- api_docs/global_search.mdx | 2 +- api_docs/guided_onboarding.devdocs.json | 32 +- api_docs/guided_onboarding.mdx | 4 +- api_docs/home.mdx | 2 +- api_docs/image_embeddable.mdx | 2 +- api_docs/index_lifecycle_management.mdx | 2 +- api_docs/index_management.mdx | 2 +- api_docs/infra.mdx | 2 +- api_docs/inspector.mdx | 2 +- api_docs/interactive_setup.mdx | 2 +- api_docs/kbn_ace.mdx | 2 +- api_docs/kbn_aiops_components.mdx | 2 +- api_docs/kbn_aiops_utils.mdx | 2 +- api_docs/kbn_alerting_state_types.mdx | 2 +- api_docs/kbn_alerts.mdx | 2 +- api_docs/kbn_alerts_as_data_utils.mdx | 2 +- api_docs/kbn_alerts_ui_shared.mdx | 2 +- api_docs/kbn_analytics.mdx | 2 +- api_docs/kbn_analytics_client.devdocs.json | 8 + api_docs/kbn_analytics_client.mdx | 2 +- ..._analytics_shippers_elastic_v3_browser.mdx | 2 +- ...n_analytics_shippers_elastic_v3_common.mdx | 2 +- ...n_analytics_shippers_elastic_v3_server.mdx | 2 +- api_docs/kbn_analytics_shippers_fullstory.mdx | 2 +- api_docs/kbn_analytics_shippers_gainsight.mdx | 2 +- api_docs/kbn_apm_config_loader.mdx | 2 +- api_docs/kbn_apm_synthtrace.mdx | 2 +- .../kbn_apm_synthtrace_client.devdocs.json | 18 +- api_docs/kbn_apm_synthtrace_client.mdx | 4 +- api_docs/kbn_apm_utils.mdx | 2 +- api_docs/kbn_axe_config.mdx | 2 +- api_docs/kbn_cases_components.mdx | 2 +- api_docs/kbn_cell_actions.mdx | 2 +- api_docs/kbn_chart_expressions_common.mdx | 2 +- api_docs/kbn_chart_icons.mdx | 2 +- api_docs/kbn_ci_stats_core.mdx | 2 +- api_docs/kbn_ci_stats_performance_metrics.mdx | 2 +- api_docs/kbn_ci_stats_reporter.mdx | 2 +- api_docs/kbn_cli_dev_mode.mdx | 2 +- api_docs/kbn_code_editor.mdx | 2 +- api_docs/kbn_code_editor_mocks.mdx | 2 +- api_docs/kbn_coloring.mdx | 2 +- api_docs/kbn_config.mdx | 2 +- api_docs/kbn_config_mocks.mdx | 2 +- api_docs/kbn_config_schema.mdx | 2 +- .../kbn_content_management_content_editor.mdx | 2 +- ...content_management_table_list.devdocs.json | 2 +- .../kbn_content_management_table_list.mdx | 2 +- api_docs/kbn_core_analytics_browser.mdx | 2 +- .../kbn_core_analytics_browser_internal.mdx | 2 +- api_docs/kbn_core_analytics_browser_mocks.mdx | 2 +- api_docs/kbn_core_analytics_server.mdx | 2 +- .../kbn_core_analytics_server_internal.mdx | 2 +- api_docs/kbn_core_analytics_server_mocks.mdx | 2 +- api_docs/kbn_core_application_browser.mdx | 2 +- .../kbn_core_application_browser_internal.mdx | 2 +- .../kbn_core_application_browser_mocks.mdx | 2 +- api_docs/kbn_core_application_common.mdx | 2 +- api_docs/kbn_core_apps_browser_internal.mdx | 2 +- api_docs/kbn_core_apps_browser_mocks.mdx | 2 +- api_docs/kbn_core_apps_server_internal.mdx | 2 +- api_docs/kbn_core_base_browser_mocks.mdx | 2 +- api_docs/kbn_core_base_common.mdx | 2 +- api_docs/kbn_core_base_server_internal.mdx | 2 +- api_docs/kbn_core_base_server_mocks.mdx | 2 +- .../kbn_core_capabilities_browser_mocks.mdx | 2 +- api_docs/kbn_core_capabilities_common.mdx | 2 +- api_docs/kbn_core_capabilities_server.mdx | 2 +- .../kbn_core_capabilities_server_mocks.mdx | 2 +- api_docs/kbn_core_chrome_browser.mdx | 2 +- api_docs/kbn_core_chrome_browser_mocks.mdx | 2 +- api_docs/kbn_core_config_server_internal.mdx | 2 +- api_docs/kbn_core_custom_branding_browser.mdx | 2 +- ..._core_custom_branding_browser_internal.mdx | 2 +- ...kbn_core_custom_branding_browser_mocks.mdx | 2 +- api_docs/kbn_core_custom_branding_common.mdx | 2 +- api_docs/kbn_core_custom_branding_server.mdx | 2 +- ...n_core_custom_branding_server_internal.mdx | 2 +- .../kbn_core_custom_branding_server_mocks.mdx | 2 +- api_docs/kbn_core_deprecations_browser.mdx | 2 +- ...kbn_core_deprecations_browser_internal.mdx | 2 +- .../kbn_core_deprecations_browser_mocks.mdx | 2 +- api_docs/kbn_core_deprecations_common.mdx | 2 +- api_docs/kbn_core_deprecations_server.mdx | 2 +- .../kbn_core_deprecations_server_internal.mdx | 2 +- .../kbn_core_deprecations_server_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_browser.mdx | 2 +- api_docs/kbn_core_doc_links_browser_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_server.mdx | 2 +- api_docs/kbn_core_doc_links_server_mocks.mdx | 2 +- ...e_elasticsearch_client_server_internal.mdx | 2 +- ...core_elasticsearch_client_server_mocks.mdx | 2 +- api_docs/kbn_core_elasticsearch_server.mdx | 2 +- ...kbn_core_elasticsearch_server_internal.mdx | 2 +- .../kbn_core_elasticsearch_server_mocks.mdx | 2 +- .../kbn_core_environment_server_internal.mdx | 2 +- .../kbn_core_environment_server_mocks.mdx | 2 +- .../kbn_core_execution_context_browser.mdx | 2 +- ...ore_execution_context_browser_internal.mdx | 2 +- ...n_core_execution_context_browser_mocks.mdx | 2 +- .../kbn_core_execution_context_common.mdx | 2 +- .../kbn_core_execution_context_server.mdx | 2 +- ...core_execution_context_server_internal.mdx | 2 +- ...bn_core_execution_context_server_mocks.mdx | 2 +- api_docs/kbn_core_fatal_errors_browser.mdx | 2 +- .../kbn_core_fatal_errors_browser_mocks.mdx | 2 +- api_docs/kbn_core_http_browser.mdx | 2 +- api_docs/kbn_core_http_browser_internal.mdx | 2 +- api_docs/kbn_core_http_browser_mocks.mdx | 2 +- api_docs/kbn_core_http_common.mdx | 2 +- .../kbn_core_http_context_server_mocks.mdx | 2 +- ...re_http_request_handler_context_server.mdx | 2 +- api_docs/kbn_core_http_resources_server.mdx | 2 +- ...bn_core_http_resources_server_internal.mdx | 2 +- .../kbn_core_http_resources_server_mocks.mdx | 2 +- .../kbn_core_http_router_server_internal.mdx | 2 +- .../kbn_core_http_router_server_mocks.mdx | 2 +- api_docs/kbn_core_http_server.mdx | 2 +- api_docs/kbn_core_http_server_internal.mdx | 2 +- api_docs/kbn_core_http_server_mocks.mdx | 2 +- api_docs/kbn_core_i18n_browser.mdx | 2 +- api_docs/kbn_core_i18n_browser_mocks.mdx | 2 +- api_docs/kbn_core_i18n_server.mdx | 2 +- api_docs/kbn_core_i18n_server_internal.mdx | 2 +- api_docs/kbn_core_i18n_server_mocks.mdx | 2 +- ...n_core_injected_metadata_browser_mocks.mdx | 2 +- ...kbn_core_integrations_browser_internal.mdx | 2 +- .../kbn_core_integrations_browser_mocks.mdx | 2 +- api_docs/kbn_core_lifecycle_browser.mdx | 2 +- api_docs/kbn_core_lifecycle_browser_mocks.mdx | 2 +- api_docs/kbn_core_lifecycle_server.mdx | 2 +- api_docs/kbn_core_lifecycle_server_mocks.mdx | 2 +- api_docs/kbn_core_logging_browser_mocks.mdx | 2 +- api_docs/kbn_core_logging_common_internal.mdx | 2 +- api_docs/kbn_core_logging_server.mdx | 2 +- api_docs/kbn_core_logging_server_internal.mdx | 2 +- api_docs/kbn_core_logging_server_mocks.mdx | 2 +- ...ore_metrics_collectors_server_internal.mdx | 2 +- ...n_core_metrics_collectors_server_mocks.mdx | 2 +- api_docs/kbn_core_metrics_server.mdx | 2 +- api_docs/kbn_core_metrics_server_internal.mdx | 2 +- api_docs/kbn_core_metrics_server_mocks.mdx | 2 +- api_docs/kbn_core_mount_utils_browser.mdx | 2 +- api_docs/kbn_core_node_server.mdx | 2 +- api_docs/kbn_core_node_server_internal.mdx | 2 +- api_docs/kbn_core_node_server_mocks.mdx | 2 +- api_docs/kbn_core_notifications_browser.mdx | 2 +- ...bn_core_notifications_browser_internal.mdx | 2 +- .../kbn_core_notifications_browser_mocks.mdx | 2 +- api_docs/kbn_core_overlays_browser.mdx | 2 +- .../kbn_core_overlays_browser_internal.mdx | 2 +- api_docs/kbn_core_overlays_browser_mocks.mdx | 2 +- api_docs/kbn_core_plugins_browser.mdx | 2 +- api_docs/kbn_core_plugins_browser_mocks.mdx | 2 +- api_docs/kbn_core_plugins_server.mdx | 2 +- api_docs/kbn_core_plugins_server_mocks.mdx | 2 +- api_docs/kbn_core_preboot_server.mdx | 2 +- api_docs/kbn_core_preboot_server_mocks.mdx | 2 +- api_docs/kbn_core_rendering_browser_mocks.mdx | 2 +- .../kbn_core_rendering_server_internal.mdx | 2 +- api_docs/kbn_core_rendering_server_mocks.mdx | 2 +- api_docs/kbn_core_root_server_internal.mdx | 2 +- .../kbn_core_saved_objects_api_browser.mdx | 2 +- .../kbn_core_saved_objects_api_server.mdx | 2 +- ...core_saved_objects_api_server_internal.mdx | 2 +- ...bn_core_saved_objects_api_server_mocks.mdx | 2 +- ...ore_saved_objects_base_server_internal.mdx | 2 +- ...n_core_saved_objects_base_server_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_browser.mdx | 2 +- ...bn_core_saved_objects_browser_internal.mdx | 2 +- .../kbn_core_saved_objects_browser_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_common.mdx | 2 +- ..._objects_import_export_server_internal.mdx | 2 +- ...ved_objects_import_export_server_mocks.mdx | 2 +- ...aved_objects_migration_server_internal.mdx | 2 +- ...e_saved_objects_migration_server_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_server.mdx | 2 +- ...kbn_core_saved_objects_server_internal.mdx | 2 +- .../kbn_core_saved_objects_server_mocks.mdx | 2 +- .../kbn_core_saved_objects_utils_server.mdx | 2 +- api_docs/kbn_core_status_common.mdx | 2 +- api_docs/kbn_core_status_common_internal.mdx | 2 +- api_docs/kbn_core_status_server.mdx | 2 +- api_docs/kbn_core_status_server_internal.mdx | 2 +- api_docs/kbn_core_status_server_mocks.mdx | 2 +- ...core_test_helpers_deprecations_getters.mdx | 2 +- ...n_core_test_helpers_http_setup_browser.mdx | 2 +- api_docs/kbn_core_test_helpers_kbn_server.mdx | 2 +- ...n_core_test_helpers_so_type_serializer.mdx | 2 +- api_docs/kbn_core_test_helpers_test_utils.mdx | 2 +- api_docs/kbn_core_theme_browser.mdx | 2 +- api_docs/kbn_core_theme_browser_internal.mdx | 2 +- api_docs/kbn_core_theme_browser_mocks.mdx | 2 +- api_docs/kbn_core_ui_settings_browser.mdx | 2 +- .../kbn_core_ui_settings_browser_internal.mdx | 2 +- .../kbn_core_ui_settings_browser_mocks.mdx | 2 +- api_docs/kbn_core_ui_settings_common.mdx | 2 +- api_docs/kbn_core_ui_settings_server.mdx | 2 +- .../kbn_core_ui_settings_server_internal.mdx | 2 +- .../kbn_core_ui_settings_server_mocks.mdx | 2 +- api_docs/kbn_core_usage_data_server.mdx | 2 +- .../kbn_core_usage_data_server_internal.mdx | 2 +- api_docs/kbn_core_usage_data_server_mocks.mdx | 2 +- api_docs/kbn_crypto.mdx | 2 +- api_docs/kbn_crypto_browser.mdx | 2 +- api_docs/kbn_cypress_config.mdx | 2 +- api_docs/kbn_datemath.mdx | 2 +- api_docs/kbn_dev_cli_errors.mdx | 2 +- api_docs/kbn_dev_cli_runner.mdx | 2 +- api_docs/kbn_dev_proc_runner.mdx | 2 +- api_docs/kbn_dev_utils.mdx | 2 +- api_docs/kbn_doc_links.mdx | 2 +- api_docs/kbn_docs_utils.mdx | 2 +- api_docs/kbn_dom_drag_drop.mdx | 2 +- api_docs/kbn_ebt_tools.mdx | 2 +- api_docs/kbn_ecs.mdx | 2 +- api_docs/kbn_ecs_data_quality_dashboard.mdx | 2 +- api_docs/kbn_es.mdx | 2 +- api_docs/kbn_es_archiver.mdx | 2 +- api_docs/kbn_es_errors.mdx | 2 +- api_docs/kbn_es_query.devdocs.json | 4 +- api_docs/kbn_es_query.mdx | 2 +- api_docs/kbn_es_types.mdx | 2 +- api_docs/kbn_eslint_plugin_imports.mdx | 2 +- api_docs/kbn_expandable_flyout.devdocs.json | 470 +++++++++- api_docs/kbn_expandable_flyout.mdx | 10 +- api_docs/kbn_field_types.mdx | 2 +- api_docs/kbn_find_used_node_modules.mdx | 2 +- .../kbn_ftr_common_functional_services.mdx | 2 +- api_docs/kbn_generate.mdx | 2 +- api_docs/kbn_generate_csv.mdx | 2 +- api_docs/kbn_generate_csv_types.mdx | 2 +- api_docs/kbn_guided_onboarding.devdocs.json | 44 +- api_docs/kbn_guided_onboarding.mdx | 4 +- api_docs/kbn_handlebars.mdx | 2 +- api_docs/kbn_hapi_mocks.mdx | 2 +- api_docs/kbn_health_gateway_server.mdx | 2 +- api_docs/kbn_home_sample_data_card.mdx | 2 +- api_docs/kbn_home_sample_data_tab.mdx | 2 +- api_docs/kbn_i18n.mdx | 2 +- api_docs/kbn_i18n_react.mdx | 2 +- api_docs/kbn_import_resolver.mdx | 2 +- api_docs/kbn_interpreter.mdx | 2 +- api_docs/kbn_io_ts_utils.mdx | 2 +- api_docs/kbn_jest_serializers.mdx | 2 +- api_docs/kbn_journeys.mdx | 2 +- api_docs/kbn_json_ast.mdx | 2 +- api_docs/kbn_kibana_manifest_schema.mdx | 2 +- .../kbn_language_documentation_popover.mdx | 2 +- api_docs/kbn_logging.mdx | 2 +- api_docs/kbn_logging_mocks.mdx | 2 +- api_docs/kbn_managed_vscode_config.mdx | 2 +- api_docs/kbn_mapbox_gl.mdx | 2 +- api_docs/kbn_ml_agg_utils.mdx | 2 +- api_docs/kbn_ml_date_picker.mdx | 2 +- api_docs/kbn_ml_error_utils.devdocs.json | 800 ++++++++++++++++++ api_docs/kbn_ml_error_utils.mdx | 39 + api_docs/kbn_ml_is_defined.mdx | 2 +- api_docs/kbn_ml_is_populated_object.mdx | 2 +- api_docs/kbn_ml_local_storage.mdx | 2 +- api_docs/kbn_ml_nested_property.mdx | 2 +- api_docs/kbn_ml_number_utils.mdx | 2 +- api_docs/kbn_ml_query_utils.mdx | 2 +- api_docs/kbn_ml_random_sampler_utils.mdx | 2 +- api_docs/kbn_ml_route_utils.mdx | 2 +- api_docs/kbn_ml_string_hash.mdx | 2 +- api_docs/kbn_ml_trained_models_utils.mdx | 2 +- api_docs/kbn_ml_url_state.mdx | 2 +- api_docs/kbn_monaco.mdx | 2 +- api_docs/kbn_object_versioning.devdocs.json | 44 + api_docs/kbn_object_versioning.mdx | 4 +- api_docs/kbn_observability_alert_details.mdx | 2 +- api_docs/kbn_optimizer.mdx | 2 +- api_docs/kbn_optimizer_webpack_helpers.mdx | 2 +- api_docs/kbn_osquery_io_ts_types.mdx | 2 +- ..._performance_testing_dataset_extractor.mdx | 2 +- api_docs/kbn_plugin_generator.mdx | 2 +- api_docs/kbn_plugin_helpers.mdx | 2 +- api_docs/kbn_react_field.mdx | 2 +- api_docs/kbn_repo_file_maps.mdx | 2 +- api_docs/kbn_repo_linter.mdx | 2 +- api_docs/kbn_repo_path.mdx | 2 +- api_docs/kbn_repo_source_classifier.mdx | 2 +- api_docs/kbn_reporting_common.mdx | 2 +- api_docs/kbn_rison.mdx | 2 +- api_docs/kbn_rule_data_utils.mdx | 2 +- api_docs/kbn_saved_objects_settings.mdx | 2 +- api_docs/kbn_security_solution_side_nav.mdx | 2 +- ...kbn_security_solution_storybook_config.mdx | 2 +- .../kbn_securitysolution_autocomplete.mdx | 2 +- api_docs/kbn_securitysolution_data_table.mdx | 2 +- api_docs/kbn_securitysolution_ecs.mdx | 2 +- api_docs/kbn_securitysolution_es_utils.mdx | 2 +- ...ritysolution_exception_list_components.mdx | 2 +- api_docs/kbn_securitysolution_grouping.mdx | 2 +- api_docs/kbn_securitysolution_hook_utils.mdx | 2 +- ...solution_io_ts_alerting_types.devdocs.json | 524 +++++++++++- ..._securitysolution_io_ts_alerting_types.mdx | 4 +- ...ritysolution_io_ts_list_types.devdocs.json | 336 +++++++- .../kbn_securitysolution_io_ts_list_types.mdx | 4 +- api_docs/kbn_securitysolution_io_ts_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_utils.mdx | 2 +- ...kbn_securitysolution_list_api.devdocs.json | 51 ++ api_docs/kbn_securitysolution_list_api.mdx | 4 +- .../kbn_securitysolution_list_constants.mdx | 2 +- ...n_securitysolution_list_hooks.devdocs.json | 64 +- api_docs/kbn_securitysolution_list_hooks.mdx | 4 +- api_docs/kbn_securitysolution_list_utils.mdx | 2 +- api_docs/kbn_securitysolution_rules.mdx | 2 +- api_docs/kbn_securitysolution_t_grid.mdx | 2 +- api_docs/kbn_securitysolution_utils.mdx | 2 +- api_docs/kbn_server_http_tools.mdx | 2 +- api_docs/kbn_server_route_repository.mdx | 2 +- api_docs/kbn_shared_svg.mdx | 2 +- api_docs/kbn_shared_ux_avatar_solution.mdx | 2 +- ...ared_ux_avatar_user_profile_components.mdx | 2 +- .../kbn_shared_ux_button_exit_full_screen.mdx | 2 +- ...hared_ux_button_exit_full_screen_mocks.mdx | 2 +- api_docs/kbn_shared_ux_button_toolbar.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_context.mdx | 2 +- api_docs/kbn_shared_ux_file_image.mdx | 2 +- api_docs/kbn_shared_ux_file_image_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_picker.mdx | 2 +- api_docs/kbn_shared_ux_file_types.mdx | 2 +- api_docs/kbn_shared_ux_file_upload.mdx | 2 +- api_docs/kbn_shared_ux_file_util.mdx | 2 +- api_docs/kbn_shared_ux_link_redirect_app.mdx | 2 +- .../kbn_shared_ux_link_redirect_app_mocks.mdx | 2 +- api_docs/kbn_shared_ux_markdown.mdx | 2 +- api_docs/kbn_shared_ux_markdown_mocks.mdx | 2 +- .../kbn_shared_ux_page_analytics_no_data.mdx | 2 +- ...shared_ux_page_analytics_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_page_kibana_no_data.mdx | 2 +- ...bn_shared_ux_page_kibana_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_page_kibana_template.mdx | 2 +- ...n_shared_ux_page_kibana_template_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_no_data.mdx | 2 +- .../kbn_shared_ux_page_no_data_config.mdx | 2 +- ...bn_shared_ux_page_no_data_config_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_no_data_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_solution_nav.mdx | 2 +- .../kbn_shared_ux_prompt_no_data_views.mdx | 2 +- ...n_shared_ux_prompt_no_data_views_mocks.mdx | 2 +- api_docs/kbn_shared_ux_prompt_not_found.mdx | 2 +- api_docs/kbn_shared_ux_router.mdx | 2 +- api_docs/kbn_shared_ux_router_mocks.mdx | 2 +- api_docs/kbn_shared_ux_storybook_config.mdx | 2 +- api_docs/kbn_shared_ux_storybook_mock.mdx | 2 +- api_docs/kbn_shared_ux_utility.mdx | 2 +- api_docs/kbn_slo_schema.mdx | 2 +- api_docs/kbn_some_dev_log.mdx | 2 +- api_docs/kbn_std.mdx | 2 +- api_docs/kbn_stdio_dev_helpers.mdx | 2 +- api_docs/kbn_storybook.mdx | 2 +- api_docs/kbn_telemetry_tools.mdx | 2 +- api_docs/kbn_test.mdx | 2 +- api_docs/kbn_test_jest_helpers.mdx | 2 +- api_docs/kbn_test_subj_selector.mdx | 2 +- api_docs/kbn_tooling_log.mdx | 2 +- api_docs/kbn_ts_projects.mdx | 2 +- api_docs/kbn_typed_react_router_config.mdx | 2 +- api_docs/kbn_ui_actions_browser.mdx | 2 +- api_docs/kbn_ui_shared_deps_src.mdx | 2 +- api_docs/kbn_ui_theme.mdx | 2 +- api_docs/kbn_url_state.devdocs.json | 99 +++ api_docs/kbn_url_state.mdx | 30 + api_docs/kbn_user_profile_components.mdx | 2 +- api_docs/kbn_utility_types.mdx | 2 +- api_docs/kbn_utility_types_jest.mdx | 2 +- api_docs/kbn_utils.mdx | 2 +- api_docs/kbn_yarn_lock_validator.mdx | 2 +- api_docs/kibana_overview.mdx | 2 +- api_docs/kibana_react.mdx | 2 +- api_docs/kibana_utils.mdx | 2 +- api_docs/kubernetes_security.mdx | 2 +- api_docs/lens.mdx | 2 +- api_docs/license_api_guard.mdx | 2 +- api_docs/license_management.mdx | 2 +- api_docs/licensing.mdx | 2 +- api_docs/lists.devdocs.json | 4 +- api_docs/lists.mdx | 2 +- api_docs/management.mdx | 2 +- api_docs/maps.mdx | 2 +- api_docs/maps_ems.mdx | 2 +- api_docs/ml.devdocs.json | 35 - api_docs/ml.mdx | 4 +- api_docs/monitoring.mdx | 2 +- api_docs/monitoring_collection.mdx | 2 +- api_docs/navigation.mdx | 2 +- api_docs/newsfeed.mdx | 2 +- api_docs/notifications.mdx | 2 +- api_docs/observability.mdx | 2 +- api_docs/observability_shared.mdx | 2 +- api_docs/osquery.mdx | 2 +- api_docs/plugin_directory.mdx | 34 +- api_docs/presentation_util.mdx | 2 +- api_docs/profiling.mdx | 2 +- api_docs/remote_clusters.mdx | 2 +- api_docs/reporting.mdx | 2 +- api_docs/rollup.mdx | 2 +- api_docs/rule_registry.mdx | 2 +- api_docs/runtime_fields.mdx | 2 +- api_docs/saved_objects.mdx | 2 +- api_docs/saved_objects_finder.mdx | 2 +- api_docs/saved_objects_management.mdx | 2 +- api_docs/saved_objects_tagging.mdx | 2 +- api_docs/saved_objects_tagging_oss.mdx | 2 +- api_docs/saved_search.mdx | 2 +- api_docs/screenshot_mode.mdx | 2 +- api_docs/screenshotting.mdx | 2 +- api_docs/security.mdx | 2 +- api_docs/security_solution.mdx | 2 +- api_docs/session_view.mdx | 2 +- api_docs/share.mdx | 2 +- api_docs/snapshot_restore.mdx | 2 +- api_docs/spaces.mdx | 2 +- api_docs/stack_alerts.mdx | 2 +- api_docs/stack_connectors.mdx | 2 +- api_docs/task_manager.mdx | 2 +- api_docs/telemetry.mdx | 2 +- api_docs/telemetry_collection_manager.mdx | 2 +- api_docs/telemetry_collection_xpack.mdx | 2 +- api_docs/telemetry_management_section.mdx | 2 +- api_docs/threat_intelligence.mdx | 2 +- api_docs/timelines.mdx | 2 +- api_docs/transform.mdx | 2 +- api_docs/triggers_actions_ui.devdocs.json | 25 + api_docs/triggers_actions_ui.mdx | 4 +- api_docs/ui_actions.mdx | 2 +- api_docs/ui_actions_enhanced.mdx | 2 +- api_docs/unified_field_list.mdx | 2 +- api_docs/unified_histogram.mdx | 2 +- api_docs/unified_search.devdocs.json | 2 +- api_docs/unified_search.mdx | 2 +- api_docs/unified_search_autocomplete.mdx | 2 +- api_docs/url_forwarding.mdx | 2 +- api_docs/usage_collection.mdx | 2 +- api_docs/ux.mdx | 2 +- api_docs/vis_default_editor.mdx | 2 +- api_docs/vis_type_gauge.mdx | 2 +- api_docs/vis_type_heatmap.mdx | 2 +- api_docs/vis_type_pie.mdx | 2 +- api_docs/vis_type_table.mdx | 2 +- api_docs/vis_type_timelion.mdx | 2 +- api_docs/vis_type_timeseries.mdx | 2 +- api_docs/vis_type_vega.mdx | 2 +- api_docs/vis_type_vislib.mdx | 2 +- api_docs/vis_type_xy.mdx | 2 +- api_docs/visualizations.mdx | 2 +- 522 files changed, 3240 insertions(+), 674 deletions(-) create mode 100644 api_docs/kbn_ml_error_utils.devdocs.json create mode 100644 api_docs/kbn_ml_error_utils.mdx create mode 100644 api_docs/kbn_url_state.devdocs.json create mode 100644 api_docs/kbn_url_state.mdx diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 6bfdc25b531497..9f6783794ad2ce 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index 4cf73a72168eb6..f41313a90546b3 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index c0fa0ef3e4c025..50cb3ba36668eb 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.devdocs.json b/api_docs/alerting.devdocs.json index cfec38128269ce..264f3c10d43f39 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -5261,12 +5261,20 @@ { "parentPluginId": "alerting", "id": "def-common.AlertsFilter.query", - "type": "CompoundType", + "type": "Object", "tags": [], "label": "query", "description": [], "signature": [ - "{ kql: string; dsl?: string | undefined; } | null" + "{ kql: string; filters: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]; dsl?: string | undefined; } | undefined" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, @@ -5275,7 +5283,7 @@ { "parentPluginId": "alerting", "id": "def-common.AlertsFilter.timeframe", - "type": "CompoundType", + "type": "Object", "tags": [], "label": "timeframe", "description": [], @@ -5287,7 +5295,7 @@ "section": "def-common.AlertsFilterTimeframe", "text": "AlertsFilterTimeframe" }, - " | null" + " | undefined" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, @@ -5541,6 +5549,20 @@ "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertStatus.maintenanceWindowIds", + "type": "Array", + "tags": [], + "label": "maintenanceWindowIds", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -5755,6 +5777,17 @@ "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertSummary.revision", + "type": "number", + "tags": [], + "label": "revision", + "description": [], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -6322,6 +6355,20 @@ "path": "x-pack/plugins/alerting/common/execution_log_types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.IExecutionLog.maintenance_window_ids", + "type": "Array", + "tags": [], + "label": "maintenance_window_ids", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/alerting/common/execution_log_types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -8864,12 +8911,20 @@ { "parentPluginId": "alerting", "id": "def-common.SanitizedAlertsFilter.query", - "type": "CompoundType", + "type": "Object", "tags": [], "label": "query", "description": [], "signature": [ - "{ kql: string; } | null" + "{ kql: string; filters: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]; } | undefined" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, @@ -8878,7 +8933,7 @@ { "parentPluginId": "alerting", "id": "def-common.SanitizedAlertsFilter.timeframe", - "type": "CompoundType", + "type": "Object", "tags": [], "label": "timeframe", "description": [], @@ -8890,7 +8945,7 @@ "section": "def-common.AlertsFilterTimeframe", "text": "AlertsFilterTimeframe" }, - " | null" + " | undefined" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index ad9cd7c82bac47..05cd2479d10f49 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 603 | 1 | 582 | 42 | +| 606 | 1 | 585 | 42 | ## Client diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index 93eeed1f133951..f3585cfd924b4a 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -4193,6 +4193,8 @@ "AggregationType", ".P99>]>; serviceName: ", "StringC", + "; errorGroupingKey: ", + "StringC", "; transactionType: ", "StringC", "; transactionName: ", @@ -4269,6 +4271,8 @@ "AggregationType", ".P99>]>; serviceName: ", "StringC", + "; errorGroupingKey: ", + "StringC", "; transactionType: ", "StringC", "; transactionName: ", @@ -4343,6 +4347,8 @@ "AggregationType", ".P99>]>; serviceName: ", "StringC", + "; errorGroupingKey: ", + "StringC", "; transactionType: ", "StringC", "; transactionName: ", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index a5226b9ec1a9f9..39984c4c85e921 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/asset_manager.mdx b/api_docs/asset_manager.mdx index 5704f9f7b9f814..3fdc2e696e3f07 100644 --- a/api_docs/asset_manager.mdx +++ b/api_docs/asset_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/assetManager title: "assetManager" image: https://source.unsplash.com/400x175/?github description: API docs for the assetManager plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'assetManager'] --- import assetManagerObj from './asset_manager.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index 5fcf3933483c98..f79d12ad047ef6 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index d3a45d45955535..fc6dbe517b369e 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index edb1a65ab929d3..bbeae3846322ff 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 48d5f0ea25d03d..15ec086b8eaf2f 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index e4c1c729146d89..8a55c2821385f1 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index 0ef73b05d8bb04..c09cbda53a15ce 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index 28bf235d4e8036..53a1a634088ebd 100644 --- a/api_docs/cloud_chat.mdx +++ b/api_docs/cloud_chat.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChat title: "cloudChat" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChat plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_data_migration.mdx b/api_docs/cloud_data_migration.mdx index ea00c024dabf26..cf1dca265856b4 100644 --- a/api_docs/cloud_data_migration.mdx +++ b/api_docs/cloud_data_migration.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDataMigration title: "cloudDataMigration" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDataMigration plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDataMigration'] --- import cloudDataMigrationObj from './cloud_data_migration.devdocs.json'; diff --git a/api_docs/cloud_defend.mdx b/api_docs/cloud_defend.mdx index b6680d6d7dbd12..815fae9c2fb420 100644 --- a/api_docs/cloud_defend.mdx +++ b/api_docs/cloud_defend.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDefend title: "cloudDefend" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDefend plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDefend'] --- import cloudDefendObj from './cloud_defend.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index 08bce01d730624..c56ad03988470d 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index a88a3ac25809bd..7b95ed904f226c 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index aaa30afdee1efe..2f84a61fdae2e9 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/content_management.devdocs.json b/api_docs/content_management.devdocs.json index b1fc3a2404541c..7a800426b50add 100644 --- a/api_docs/content_management.devdocs.json +++ b/api_docs/content_management.devdocs.json @@ -1259,7 +1259,7 @@ "section": "def-server.ContentStorage", "text": "ContentStorage" }, - "" + "" ], "path": "src/plugins/content_management/server/core/types.ts", "deprecated": false, @@ -1773,15 +1773,147 @@ { "parentPluginId": "contentManagement", "id": "def-server.ContentStorage.mSearch", - "type": "Object", + "type": "Uncategorized", "tags": [], "label": "mSearch", "description": [ "\nOpt-in to multi-type search.\nCan only be supported if the content type is backed by a saved object since `mSearch` is using the `savedObjects.find` API." ], "signature": [ - "MSearchConfig", - " | undefined" + "TMSearchConfig | undefined" + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig", + "type": "Interface", + "tags": [], + "label": "MSearchConfig", + "description": [ + "\nA configuration for multi-type search.\nBy configuring a content type with a `MSearchConfig`, it can be searched in the multi-type search.\nUnderneath content management is using the `savedObjects.find` API to search the saved objects." + ], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "server", + "docId": "kibContentManagementPluginApi", + "section": "def-server.MSearchConfig", + "text": "MSearchConfig" + }, + "" + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.savedObjectType", + "type": "string", + "tags": [], + "label": "savedObjectType", + "description": [ + "\nThe saved object type that corresponds to this content type." + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.toItemResult", + "type": "Function", + "tags": [], + "label": "toItemResult", + "description": [ + "\nMapper function that transforms the saved object into the content item result." + ], + "signature": [ + "(ctx: ", + { + "pluginId": "contentManagement", + "scope": "server", + "docId": "kibContentManagementPluginApi", + "section": "def-server.StorageContext", + "text": "StorageContext" + }, + ", savedObject: ", + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsFindResult", + "text": "SavedObjectsFindResult" + }, + ") => T" + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.toItemResult.$1", + "type": "Object", + "tags": [], + "label": "ctx", + "description": [], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "server", + "docId": "kibContentManagementPluginApi", + "section": "def-server.StorageContext", + "text": "StorageContext" + } + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.toItemResult.$2", + "type": "Object", + "tags": [], + "label": "savedObject", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsFindResult", + "text": "SavedObjectsFindResult" + }, + "" + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.additionalSearchFields", + "type": "Array", + "tags": [], + "label": "additionalSearchFields", + "description": [ + "\nAdditional fields to search on. These fields will be added to the search query.\nBy default, only `title` and `description` are searched." + ], + "signature": [ + "string[] | undefined" ], "path": "src/plugins/content_management/server/core/types.ts", "deprecated": false, diff --git a/api_docs/content_management.mdx b/api_docs/content_management.mdx index 1f578689d0a00f..21f71777a11e18 100644 --- a/api_docs/content_management.mdx +++ b/api_docs/content_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/contentManagement title: "contentManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the contentManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'contentManagement'] --- import contentManagementObj from './content_management.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 143 | 0 | 124 | 7 | +| 149 | 0 | 126 | 6 | ## Client diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 9e673b08f099a3..c280197a6417a7 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 5f75f99f25e247..53316962496a52 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index eee68e6d1bd7b4..ee4d46dc9145f6 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index 707e03a38207bb..e79d7c3abd944e 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.mdx b/api_docs/data.mdx index f22998f41aaf7f..f58d4af3b6e61b 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index d08cdb5429fa80..8fd1bc5eca79f4 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 810eacf23336f5..f56c297a354bad 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 9c8b060ef7919b..776cf231e1e627 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index c5097f4dc01c15..4ac05486048f37 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index ddb9680520f038..1e27fc11d34a9e 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 9301c42220b3af..57c1558717fa70 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index aca7cffe95904a..1a34b399f959c0 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index f7300841721236..b98c2fea8fc272 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index cacf63a726fb43..b49d0197cbea07 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index c210b2af4a6b31..1a29e749e4dc75 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 880a19dbd0e91a..73de9827ebc22f 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index f6cb112f22cc5a..cde28fb9b20e0c 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index b3793bce178b0a..79aea49ce02396 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/ecs_data_quality_dashboard.mdx b/api_docs/ecs_data_quality_dashboard.mdx index fb9dfb81dacaec..ec0192478b19ad 100644 --- a/api_docs/ecs_data_quality_dashboard.mdx +++ b/api_docs/ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ecsDataQualityDashboard title: "ecsDataQualityDashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the ecsDataQualityDashboard plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ecsDataQualityDashboard'] --- import ecsDataQualityDashboardObj from './ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 1dca0ef9fb7d25..1de1e6f809d6bb 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index 5826149eca6093..24c39169fe91b4 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index bc44d093b0ff41..6004d2b66f1792 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 9ad8338b79886f..68f4430bbeaaed 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 7b08e077f39e16..9e32f4ae6e75bc 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index 7d9413f9555b1f..77ff102f16bcbc 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.devdocs.json b/api_docs/event_log.devdocs.json index c4bf518f79333b..b3075cced41d85 100644 --- a/api_docs/event_log.devdocs.json +++ b/api_docs/event_log.devdocs.json @@ -1514,7 +1514,7 @@ "label": "data", "description": [], "signature": [ - "(Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" + "(Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" ], "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", "deprecated": false, @@ -1534,7 +1534,7 @@ "label": "IEvent", "description": [], "signature": [ - "DeepPartial | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, @@ -1549,7 +1549,7 @@ "label": "IValidatedEvent", "description": [], "signature": [ - "Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined" + "Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index bfe5735a5b5aa7..5e2e0f6c10292e 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/exploratory_view.mdx b/api_docs/exploratory_view.mdx index 2ebfc71ebc0605..7315c69e133779 100644 --- a/api_docs/exploratory_view.mdx +++ b/api_docs/exploratory_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/exploratoryView title: "exploratoryView" image: https://source.unsplash.com/400x175/?github description: API docs for the exploratoryView plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'exploratoryView'] --- import exploratoryViewObj from './exploratory_view.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index 5f952a34778d8f..4f23479cfbfc62 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 17b1e1a7dd0c3f..3c92eace2b8e38 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 8e23328ac3938c..0a35f4158d1793 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 2a5739fbf1ce67..92cf80d1ab61a0 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index e5641b9c5fd0d5..62c32bae5f5f1f 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index b8a570a50ce477..8ccde120fd543b 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index 7ea3ec6b861ebf..6432d4714f1e30 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 92e6b7076b1090..d8a88c5d959603 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index 5641f62c19926d..a238dd8036fd16 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index 10142b9bf030ae..333d1ba0e6264a 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 26f353bb29f5fa..079a9d1bbced99 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index 4961a72b260d78..8af091771bd3ed 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index a70dc43040e760..1f6d6a9ba1c684 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 823159e7665474..631a20f0bcdf6c 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 711def3633bf5c..6dedd249971e18 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index 0996e6259bf11d..fc537a030e52bc 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 307f54008c9918..624bbf01a981dd 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.mdx b/api_docs/files.mdx index b3f4dba8b7273b..2e7af1d73120f6 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index bbf97bb4e042f7..49c9a40ff89468 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index f1a34342d9ed31..fe95c91406e4c2 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index e5180ea54c5f9b..37e44d7a88a33e 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.devdocs.json b/api_docs/guided_onboarding.devdocs.json index cd8f47f8cf2776..a73976c10b7945 100644 --- a/api_docs/guided_onboarding.devdocs.json +++ b/api_docs/guided_onboarding.devdocs.json @@ -690,7 +690,15 @@ "section": "def-common.GuideStepIds", "text": "GuideStepIds" }, - ") => Promise<{ pluginState: ", + ", params?: ", + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideParams", + "text": "GuideParams" + }, + " | undefined) => Promise<{ pluginState: ", { "pluginId": "guidedOnboarding", "scope": "common", @@ -745,6 +753,28 @@ "deprecated": false, "trackAdoption": false, "isRequired": true + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-public.GuidedOnboardingApi.completeGuideStep.$3", + "type": "Object", + "tags": [], + "label": "params", + "description": [], + "signature": [ + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideParams", + "text": "GuideParams" + }, + " | undefined" + ], + "path": "src/plugins/guided_onboarding/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false } ], "returnComment": [] diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index d01239408c028d..475a8a2c05da36 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/pla | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 56 | 0 | 55 | 0 | +| 57 | 0 | 56 | 0 | ## Client diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 9a3c60b620a1a5..f62e10db504615 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/image_embeddable.mdx b/api_docs/image_embeddable.mdx index f46b769442430e..3477ece3819e0f 100644 --- a/api_docs/image_embeddable.mdx +++ b/api_docs/image_embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/imageEmbeddable title: "imageEmbeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the imageEmbeddable plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'imageEmbeddable'] --- import imageEmbeddableObj from './image_embeddable.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 3d5fedb9c28b12..51b3707ff4cdaa 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index c56e4e1ac00e16..7e62154cabf299 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index 2ef0550b3c7ec2..f95130c7fda77e 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index be420e800114c0..627ca307b241f4 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 77f9f7caf3d138..5794f30eaf8673 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index 9d79fbd636e026..ec79dddfe1fdbb 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ace plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index d38801a007d3ab..027622873bb5d2 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index d5b19befd4e625..3ba110b4d641eb 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerting_state_types.mdx b/api_docs/kbn_alerting_state_types.mdx index 9d8f4dce4e4e65..9209559d4c5114 100644 --- a/api_docs/kbn_alerting_state_types.mdx +++ b/api_docs/kbn_alerting_state_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-state-types title: "@kbn/alerting-state-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-state-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-state-types'] --- import kbnAlertingStateTypesObj from './kbn_alerting_state_types.devdocs.json'; diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index 496c97c35fa87c..fd625f04df24b2 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] --- import kbnAlertsObj from './kbn_alerts.devdocs.json'; diff --git a/api_docs/kbn_alerts_as_data_utils.mdx b/api_docs/kbn_alerts_as_data_utils.mdx index af3a71ef4577bf..3529d77abdd16e 100644 --- a/api_docs/kbn_alerts_as_data_utils.mdx +++ b/api_docs/kbn_alerts_as_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-as-data-utils title: "@kbn/alerts-as-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-as-data-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-as-data-utils'] --- import kbnAlertsAsDataUtilsObj from './kbn_alerts_as_data_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts_ui_shared.mdx b/api_docs/kbn_alerts_ui_shared.mdx index 73cb3b3c1b2703..c39f9c81be7533 100644 --- a/api_docs/kbn_alerts_ui_shared.mdx +++ b/api_docs/kbn_alerts_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-ui-shared title: "@kbn/alerts-ui-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-ui-shared plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-ui-shared'] --- import kbnAlertsUiSharedObj from './kbn_alerts_ui_shared.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index c41d203f9bad1d..90704b67958d8b 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.devdocs.json b/api_docs/kbn_analytics_client.devdocs.json index 82163bbf3b11d5..0671e818e1d3fb 100644 --- a/api_docs/kbn_analytics_client.devdocs.json +++ b/api_docs/kbn_analytics_client.devdocs.json @@ -762,6 +762,14 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts" + }, { "plugin": "@kbn/core-analytics-browser-mocks", "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts" diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index cdc8ea6d81beb1..5bdc58d85dc6f7 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index eaf3487e3149dc..98e6154aa8a60a 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index 9110cc32f4c315..5cedf9017c0c3f 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index df8a64c48bd63a..1900aafa94eb79 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index 05865a67ce0b7f..506c2ef17b8a8e 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_gainsight.mdx b/api_docs/kbn_analytics_shippers_gainsight.mdx index f960174f738592..51e1daf9c0a4cf 100644 --- a/api_docs/kbn_analytics_shippers_gainsight.mdx +++ b/api_docs/kbn_analytics_shippers_gainsight.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-gainsight title: "@kbn/analytics-shippers-gainsight" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-gainsight plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-gainsight'] --- import kbnAnalyticsShippersGainsightObj from './kbn_analytics_shippers_gainsight.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 8900077fd2acec..28663f15a680c0 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index c8432ebc4205c9..1b0583f4267b79 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace_client.devdocs.json b/api_docs/kbn_apm_synthtrace_client.devdocs.json index 10dcb002bbdd02..fe2bbd03ef6abd 100644 --- a/api_docs/kbn_apm_synthtrace_client.devdocs.json +++ b/api_docs/kbn_apm_synthtrace_client.devdocs.json @@ -731,7 +731,7 @@ "label": "error", "description": [], "signature": [ - "({ message, type, groupingName, }: { message: string; type?: string | undefined; groupingName?: string | undefined; }) => ", + "({ message, type }: { message: string; type?: string | undefined; }) => ", "ApmError" ], "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/instance.ts", @@ -743,7 +743,7 @@ "id": "def-common.Instance.error.$1", "type": "Object", "tags": [], - "label": "{\n message,\n type,\n groupingName,\n }", + "label": "{ message, type }", "description": [], "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/instance.ts", "deprecated": false, @@ -773,20 +773,6 @@ "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/instance.ts", "deprecated": false, "trackAdoption": false - }, - { - "parentPluginId": "@kbn/apm-synthtrace-client", - "id": "def-common.Instance.error.$1.groupingName", - "type": "string", - "tags": [], - "label": "groupingName", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/instance.ts", - "deprecated": false, - "trackAdoption": false } ] } diff --git a/api_docs/kbn_apm_synthtrace_client.mdx b/api_docs/kbn_apm_synthtrace_client.mdx index b76de3fe835c9a..080fc9faa8d699 100644 --- a/api_docs/kbn_apm_synthtrace_client.mdx +++ b/api_docs/kbn_apm_synthtrace_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace-client title: "@kbn/apm-synthtrace-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace-client plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace-client'] --- import kbnApmSynthtraceClientObj from './kbn_apm_synthtrace_client.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) for ques | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 154 | 0 | 154 | 17 | +| 153 | 0 | 153 | 17 | ## Common diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 61e2869b4e841a..9509d2d37a5f2f 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index ac22df442633d2..c9bac589f98e14 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 738eaa012d2943..9609f525002a3e 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_cell_actions.mdx b/api_docs/kbn_cell_actions.mdx index 891ae75c684ea4..936150c592f658 100644 --- a/api_docs/kbn_cell_actions.mdx +++ b/api_docs/kbn_cell_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cell-actions title: "@kbn/cell-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cell-actions plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cell-actions'] --- import kbnCellActionsObj from './kbn_cell_actions.devdocs.json'; diff --git a/api_docs/kbn_chart_expressions_common.mdx b/api_docs/kbn_chart_expressions_common.mdx index ea357626d903df..55c5bc95f33893 100644 --- a/api_docs/kbn_chart_expressions_common.mdx +++ b/api_docs/kbn_chart_expressions_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-expressions-common title: "@kbn/chart-expressions-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-expressions-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-expressions-common'] --- import kbnChartExpressionsCommonObj from './kbn_chart_expressions_common.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index d47f7c20a984c8..81991f4c3596ae 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index b003b37a45c17e..3eeac77d30d965 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index 6d8ba1ec6ec6dd..4210e48cd996cf 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index c9b004312e1fcb..dd1eeae4185e81 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index 23afc0f33949d1..674d7849ea2780 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_code_editor.mdx b/api_docs/kbn_code_editor.mdx index 2128bc39243431..2352502cb61686 100644 --- a/api_docs/kbn_code_editor.mdx +++ b/api_docs/kbn_code_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor title: "@kbn/code-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor'] --- import kbnCodeEditorObj from './kbn_code_editor.devdocs.json'; diff --git a/api_docs/kbn_code_editor_mocks.mdx b/api_docs/kbn_code_editor_mocks.mdx index 2c4aa38ed970a4..299b419bc62ee3 100644 --- a/api_docs/kbn_code_editor_mocks.mdx +++ b/api_docs/kbn_code_editor_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor-mocks title: "@kbn/code-editor-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor-mocks'] --- import kbnCodeEditorMocksObj from './kbn_code_editor_mocks.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 1f2f008178f13f..d0b05022cb683c 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 0ce1b008677ff8..25ba7ec1f2e1f3 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index b7e89d00b32403..c9cb8f54bf0b46 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 81635d8b9bd51d..11c17046984bdd 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_editor.mdx b/api_docs/kbn_content_management_content_editor.mdx index 1288f3330685d8..54ab8505a8c85a 100644 --- a/api_docs/kbn_content_management_content_editor.mdx +++ b/api_docs/kbn_content_management_content_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-editor title: "@kbn/content-management-content-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-editor plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-editor'] --- import kbnContentManagementContentEditorObj from './kbn_content_management_content_editor.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list.devdocs.json b/api_docs/kbn_content_management_table_list.devdocs.json index 937dddb17c4deb..8f1e0a68c22aef 100644 --- a/api_docs/kbn_content_management_table_list.devdocs.json +++ b/api_docs/kbn_content_management_table_list.devdocs.json @@ -177,7 +177,7 @@ "CoreStart contract" ], "signature": [ - "{ application: { capabilities: { advancedSettings?: { save: boolean; } | undefined; }; getUrlForApp: (app: string, options: { path: string; }) => string; currentAppId$: ", + "{ application: { capabilities: { [key: string]: Readonly>>; }; getUrlForApp: (app: string, options: { path: string; }) => string; currentAppId$: ", "Observable", "; navigateToUrl: (url: string) => void | Promise; }; notifications: { toasts: { addDanger: (notifyArgs: { title: ", { diff --git a/api_docs/kbn_content_management_table_list.mdx b/api_docs/kbn_content_management_table_list.mdx index 3dfb0e5bff18e5..4c8960148995de 100644 --- a/api_docs/kbn_content_management_table_list.mdx +++ b/api_docs/kbn_content_management_table_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list title: "@kbn/content-management-table-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list'] --- import kbnContentManagementTableListObj from './kbn_content_management_table_list.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 5562e1bf3070f5..61fd9c2863537c 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index d81b99987512dd..8a3895b5351715 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index fb39ec52f54595..685a28bea6242e 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 5a1af3d3f7d7d0..5db1b0e6e16a68 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index 111fe6d8e5318a..4272953c4d7634 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index d7be4f034346ab..e0082181ed9499 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index b57a29bb0ad0a7..d3f1f8cfa09250 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index 00a6fa0cb815da..6ff37b3089b427 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index e1a48c61d3b9bb..02a3fd158ec196 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index 4bad226894e0c9..d6a70b2b0157f7 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index 5c042475509cba..d7c038b7a711f4 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index 69f9212722cc9e..eb6b4e1963417a 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index 65e3d10226f52f..1fcfd9f49e0fec 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 86f50bf6676041..7d8bb7252a8664 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index cf75d243d2b43a..69cc08dec91906 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 16fa31f01e292b..eef24003ab6608 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 4ca1597fcfadc2..58aff21c3f6337 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index a86cc6571c8319..14f3d8712b6f38 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index 027e44a2e349d0..d0b5db8e3f5b1d 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index 492519d0fef3a1..ced47f174e1bf7 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index a8ef5c7e24843e..236d416f8d3f55 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index cf04b0d4146b00..c286a69723583c 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index c3ba2d14d0f84a..c4201d23a52f5e 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index 69c93fed8bdf66..5524f5d6c7f3aa 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser.mdx b/api_docs/kbn_core_custom_branding_browser.mdx index 3447e44fdeb149..c06544f0947568 100644 --- a/api_docs/kbn_core_custom_branding_browser.mdx +++ b/api_docs/kbn_core_custom_branding_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser title: "@kbn/core-custom-branding-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser'] --- import kbnCoreCustomBrandingBrowserObj from './kbn_core_custom_branding_browser.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_internal.mdx b/api_docs/kbn_core_custom_branding_browser_internal.mdx index 512e0a4658e114..47db10d316e4b7 100644 --- a/api_docs/kbn_core_custom_branding_browser_internal.mdx +++ b/api_docs/kbn_core_custom_branding_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-internal title: "@kbn/core-custom-branding-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-internal'] --- import kbnCoreCustomBrandingBrowserInternalObj from './kbn_core_custom_branding_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_mocks.mdx b/api_docs/kbn_core_custom_branding_browser_mocks.mdx index 57674f1a86f547..74eb25b3e0989c 100644 --- a/api_docs/kbn_core_custom_branding_browser_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-mocks title: "@kbn/core-custom-branding-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-mocks'] --- import kbnCoreCustomBrandingBrowserMocksObj from './kbn_core_custom_branding_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_common.mdx b/api_docs/kbn_core_custom_branding_common.mdx index 3271ecbc408356..2852e74f1d2725 100644 --- a/api_docs/kbn_core_custom_branding_common.mdx +++ b/api_docs/kbn_core_custom_branding_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-common title: "@kbn/core-custom-branding-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-common'] --- import kbnCoreCustomBrandingCommonObj from './kbn_core_custom_branding_common.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server.mdx b/api_docs/kbn_core_custom_branding_server.mdx index 947830e94c31a1..6b39d835633258 100644 --- a/api_docs/kbn_core_custom_branding_server.mdx +++ b/api_docs/kbn_core_custom_branding_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server title: "@kbn/core-custom-branding-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server'] --- import kbnCoreCustomBrandingServerObj from './kbn_core_custom_branding_server.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_internal.mdx b/api_docs/kbn_core_custom_branding_server_internal.mdx index fed153ea6f806e..efcb0ef0d682e5 100644 --- a/api_docs/kbn_core_custom_branding_server_internal.mdx +++ b/api_docs/kbn_core_custom_branding_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-internal title: "@kbn/core-custom-branding-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-internal'] --- import kbnCoreCustomBrandingServerInternalObj from './kbn_core_custom_branding_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_mocks.mdx b/api_docs/kbn_core_custom_branding_server_mocks.mdx index 14c84336e55d83..1b84c3249cedb9 100644 --- a/api_docs/kbn_core_custom_branding_server_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-mocks title: "@kbn/core-custom-branding-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-mocks'] --- import kbnCoreCustomBrandingServerMocksObj from './kbn_core_custom_branding_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index f5dac83c7c8593..d9183c76749eb1 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index bd675bc3f02fb8..2dabde1158feb7 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index 41ca6aa3b4e820..1f150624000c09 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index 5d235b665bccf7..f6098418f65b87 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index e11a7361be6fe8..18f41498f20227 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index 86023b4693bdaa..4da7cae29afc17 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index 1b1996813ec5e1..7efaa1d260ede8 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index 014a6cff4fb8fe..3a8b4f7733b890 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 8a827ee08a3cb9..6d30b7f184a66f 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index cf25c21fc3e7b7..7e04c291cf2e54 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index d81566d93b87b8..bd6d7e53603552 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index 52c845ba3e1595..108a226e773287 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index 25bdcf097fc8f4..36da5cccdf92f8 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index d3e52e4498e97f..a54b680f5d90df 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index d7116ffefd9e3d..22c91cf01d3754 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index 8763333e3c1cac..50e70544b210b7 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index c72123128deb88..f538a5f8e0b4ef 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index 2bca835abcc60e..8f1461db2e66f9 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index e0133ab2eceac4..77a377a7db9c45 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index f4285539090c79..d7dcc38efe4717 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index e88005161eee45..d1ac9f6597bd68 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index 9bb759d225d183..4d4c0cdc35fd77 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index 391aab7e7c2075..d81bd38aa19168 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index b0a32a23bc8fd2..05d5f2ae35765b 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index d044bcd3135977..68eb012274a212 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index 3185c3568f7cdc..4bdeef98bbfdeb 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index 6d4e823e86bc73..d94568e3ed0e60 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index 2379a6b30140a0..306f52219527d6 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index 3a8785ce36e04e..ab7a2167ee8d30 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index cd9f5e1f32fae9..3a484f5c5bb947 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index 030701d5c55353..4c6f719e62bb63 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index 3041948b6c08ea..867ab6116747a7 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index 6c55bb1acc35f6..ad72f4f7242bc6 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index 1fc1939592f1d7..8bca969b220645 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index 1ce4c26ce2a2e7..408b53256f7dbb 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index c4dd257493cdb4..c87dfa9f9fccfb 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index 7baa129a75c92a..f165267f56e336 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index 9115db4edf6ccd..e9e7dc64f09420 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index 152820cbb488bb..f0cdaa85851dc4 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index 1dc544d47e7819..f38f74d167a987 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index 12f4523bca3e3e..c59347545a3283 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index 290dc43e0f6a48..c38585619f6c07 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 1892f31f906524..0fe31a6d87fa12 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index 8cd6df89cee80f..a9a56e81b982df 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index 71388b8df33ef7..ec02bcea6b7f70 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index b46f5cadc7bf91..1a2c897736245c 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index 188df6cc9052d8..3c162c91897528 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index 75b8dc9819d5a2..494208c4c59143 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index a9e58cbc2c4fb9..a037d65732f722 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index d29ee50a427bf4..99067d79f0a9f2 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index a3f5c62d35e879..534bb78d4eaba5 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index 336ba1167e4711..81ac149482e3c4 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index ead03154da97c7..26ba420c08d5c3 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index 70dcc851744314..9ba0f825772fa8 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index eaaab07d662162..a960a65461e1d5 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 764140b0a5e6c4..3fd09f6263c27f 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index 96a65db189368b..f3acbc7fc8045a 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index 7edad6a24183a3..c3ea743d26d841 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index 1bed7b5faea0ab..71e8faae9eed43 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index f8d4fffbfe0212..2eabbc2bacc62c 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index daf89498a1cfc1..f37d52033dd7a8 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index 2faf2f55b3dae3..a7217eaf16db7b 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 9a208c0343c78c..59ea42f319e9db 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index 22a3bada696766..466745e2ce4992 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index a85a0e176e8367..98986e42b5bb04 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index 0418f4ca39d608..d8b70870879305 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index fef44b08e7f61d..b45a40d461b522 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index 5b4c9e57d0c5eb..c8536a941c46f7 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index 1c53d4f1151a24..7146b20a88240c 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index ac5a7685484701..2518700bd2a9d4 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index 6538de06755815..38f32ad1bd2c1a 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index 2be250444e13d9..d158e2d4ff9534 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index 36a17d070c6e5b..e2c79208c06749 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index 7a0bd26bc2490a..8d655eb267f5a1 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index 94f1a573c6e085..77ea3d979a1fa4 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 20d7ec12378e10..44b22b20623c48 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index 1186e3108b3e37..c032049f2dd4f3 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 9e81829a0ef677..08eca2b5022064 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index 920f32db8a28ef..971dc60fbd4027 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index ff75e7da8fd402..f7cb4f618bbf9f 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index 5d8f7df7ac1769..91d2f9cd5b3a77 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index fc7ce48733faa4..e114fbfb6c8709 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index 66f6d3135417b7..12234cdb3c57c5 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index 814eebd1bcbfa7..2ae4ebd0c693ba 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 4c5f84aee2327c..55646f1f382b6c 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_internal.mdx b/api_docs/kbn_core_saved_objects_api_server_internal.mdx index 3f8241f9ab849c..165d452af821e1 100644 --- a/api_docs/kbn_core_saved_objects_api_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-internal title: "@kbn/core-saved-objects-api-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-internal'] --- import kbnCoreSavedObjectsApiServerInternalObj from './kbn_core_saved_objects_api_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index 72ad46898861df..99c35f7983158b 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index 7e7ccf32b70ab0..1978b36015f7eb 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index 76f857265d07b4..79c73b9220d0cb 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index 395382f11c5a63..10eb5868ecf479 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index e30fc42495448f..4615be9c11ce10 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index 7797116f838b6f..a37c978cfb3431 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index c7820ecbca374b..706710e2cb8245 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index 34d40c1a0e34b5..e0122dffcacb0e 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index 95f19589a3e7b0..08cc43ae6a54dc 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index 1237af41a106ab..49dcee745ce388 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index 2b1be8ab9ccf33..612f215e8d9224 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index 01110761151d4b..391665fd14f8c9 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index c1570d44d6a4e2..8f7af4ecd077da 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index b4d9023205b5bb..0a1e5d1292b0a3 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index 68054fd030ba77..b99ce47f08e8a8 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index 0099c87fe71c10..de9b1e18cae5de 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index 942ceea0dddb07..301494e7b2388d 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common-internal title: "@kbn/core-status-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index b9013fd6ca3236..df15953abc315f 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index e9c7d05fc7a4f5..075a12e1c6f248 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index 0c6549c2c40a82..d47e6f46490f11 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index 0148b222dc3078..a38afc5d4cc28a 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index 5c3b407e8d7e18..ef13c55e07c618 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index 9f82264d3416ad..cf75dcf5478b40 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index 361d7ee4abaef3..f11a869c92ee1a 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index 729bf46fe8b670..484dd22728fbc3 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 59ca3b555e77c0..63cd2de86d6e42 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_internal.mdx b/api_docs/kbn_core_theme_browser_internal.mdx index d8e9d7d490f070..54a121b8df78a2 100644 --- a/api_docs/kbn_core_theme_browser_internal.mdx +++ b/api_docs/kbn_core_theme_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-internal title: "@kbn/core-theme-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-internal'] --- import kbnCoreThemeBrowserInternalObj from './kbn_core_theme_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index fcbeaaf620f5b7..a21b5fa4ab4543 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index db1ea4bacbeffb..e741c75cc66b0a 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index 4bd232bbc6d3d4..92c21046ca2bb9 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index 50362b87de095d..9b8babfa1fcf37 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index 955203aeab79bf..b95f699d6d0503 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 5e5c93b62137f2..a88c23c489891e 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index 586bb305927ead..06432816c1788c 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index 8bde395eb141e9..e1477ab3eeb87c 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index ac875acccae8d4..914a5468667b51 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index e9e8099f4692ed..b5596d8ce5bc7f 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index d98b4e73ab7f02..c5fc08f39d31ed 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index a367c613df999b..ff42ee5669ec3a 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index 0b4c15845ebc79..6af342c88de2cd 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_cypress_config.mdx b/api_docs/kbn_cypress_config.mdx index e951de4adfb358..845a8d13f62681 100644 --- a/api_docs/kbn_cypress_config.mdx +++ b/api_docs/kbn_cypress_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cypress-config title: "@kbn/cypress-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cypress-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cypress-config'] --- import kbnCypressConfigObj from './kbn_cypress_config.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 3d864f429b0c32..3b0b33c77b2276 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index af952748b1e5a1..afafe20cb2e1e6 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index b66e4a59edd0ee..4de5774e126932 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 74b88642009bde..683f4f5f7821b8 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index df18c4a7ae5e6d..36602e1f31d550 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 237b608f7aa525..3aaf17560db473 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index d12f4055fc03a7..b1465811e22950 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_dom_drag_drop.mdx b/api_docs/kbn_dom_drag_drop.mdx index a9c6b2c6c0a281..ead5751fa35c30 100644 --- a/api_docs/kbn_dom_drag_drop.mdx +++ b/api_docs/kbn_dom_drag_drop.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dom-drag-drop title: "@kbn/dom-drag-drop" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dom-drag-drop plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dom-drag-drop'] --- import kbnDomDragDropObj from './kbn_dom_drag_drop.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index f7d904bd7bce73..17263518fb4d1a 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_ecs.mdx b/api_docs/kbn_ecs.mdx index a070106c4a4f4c..6b880006797c03 100644 --- a/api_docs/kbn_ecs.mdx +++ b/api_docs/kbn_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs title: "@kbn/ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs'] --- import kbnEcsObj from './kbn_ecs.devdocs.json'; diff --git a/api_docs/kbn_ecs_data_quality_dashboard.mdx b/api_docs/kbn_ecs_data_quality_dashboard.mdx index d6933f82a8a56d..0b8d0d939e4aea 100644 --- a/api_docs/kbn_ecs_data_quality_dashboard.mdx +++ b/api_docs/kbn_ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs-data-quality-dashboard title: "@kbn/ecs-data-quality-dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs-data-quality-dashboard plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs-data-quality-dashboard'] --- import kbnEcsDataQualityDashboardObj from './kbn_ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index b90987c9e70ef8..2969a379e7d3c4 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 9664b87b6910b4..eaf29c3077a8bd 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index 8a8c25e5981a81..ab41505be21e9c 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.devdocs.json b/api_docs/kbn_es_query.devdocs.json index 4aac051beec95b..02a3068d886271 100644 --- a/api_docs/kbn_es_query.devdocs.json +++ b/api_docs/kbn_es_query.devdocs.json @@ -4243,8 +4243,6 @@ "FilterMetaParams", " | undefined; from?: string | number | undefined; to?: string | number | undefined; gt?: string | number | undefined; lt?: string | number | undefined; gte?: string | number | undefined; lte?: string | number | undefined; format?: string | undefined; } | { query: ", "FilterMetaParams", - " | undefined; length: number; toString(): string; toLocaleString(): string; pop(): number | undefined; push(...items: number[]): number; concat(...items: ConcatArray[]): number[]; concat(...items: (number | ConcatArray)[]): number[]; join(separator?: string | undefined): string; reverse(): number[]; shift(): number | undefined; slice(start?: number | undefined, end?: number | undefined): number[]; sort(compareFn?: ((a: number, b: number) => number) | undefined): number[]; splice(start: number, deleteCount?: number | undefined): number[]; splice(start: number, deleteCount: number, ...items: number[]): number[]; unshift(...items: number[]): number; indexOf(searchElement: number, fromIndex?: number | undefined): number; lastIndexOf(searchElement: number, fromIndex?: number | undefined): number; every(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; some(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any): void; map(callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any): U[]; filter(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number[]; reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; find(predicate: (this: void, value: number, index: number, obj: number[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number | undefined; findIndex(predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number; fill(value: number, start?: number | undefined, end?: number | undefined): number[]; copyWithin(target: number, start: number, end?: number | undefined): number[]; entries(): IterableIterator<[number, number]>; keys(): IterableIterator; values(): IterableIterator; includes(searchElement: number, fromIndex?: number | undefined): boolean; flatMap(callback: (this: This, value: number, index: number, array: number[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; [Symbol.iterator](): IterableIterator; [Symbol.unscopables](): { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }; at(index: number): number | undefined; } | { query: ", - "FilterMetaParams", " | undefined; $state?: { store: ", { "pluginId": "@kbn/es-query", @@ -4263,6 +4261,8 @@ }, "; } | { query: ", "FilterMetaParams", + " | undefined; length: number; toString(): string; toLocaleString(): string; pop(): number | undefined; push(...items: number[]): number; concat(...items: ConcatArray[]): number[]; concat(...items: (number | ConcatArray)[]): number[]; join(separator?: string | undefined): string; reverse(): number[]; shift(): number | undefined; slice(start?: number | undefined, end?: number | undefined): number[]; sort(compareFn?: ((a: number, b: number) => number) | undefined): number[]; splice(start: number, deleteCount?: number | undefined): number[]; splice(start: number, deleteCount: number, ...items: number[]): number[]; unshift(...items: number[]): number; indexOf(searchElement: number, fromIndex?: number | undefined): number; lastIndexOf(searchElement: number, fromIndex?: number | undefined): number; every(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; some(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any): void; map(callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any): U[]; filter(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number[]; reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; find(predicate: (this: void, value: number, index: number, obj: number[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number | undefined; findIndex(predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number; fill(value: number, start?: number | undefined, end?: number | undefined): number[]; copyWithin(target: number, start: number, end?: number | undefined): number[]; entries(): IterableIterator<[number, number]>; keys(): IterableIterator; values(): IterableIterator; includes(searchElement: number, fromIndex?: number | undefined): boolean; flatMap(callback: (this: This, value: number, index: number, array: number[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; [Symbol.iterator](): IterableIterator; [Symbol.unscopables](): { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }; at(index: number): number | undefined; } | { query: ", + "FilterMetaParams", " | undefined; alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type: \"range\"; key?: string | undefined; params?: (", "FilterMetaParams", " & ", diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 56f69b7a094462..9dac7b564cfc45 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index 4de4378862f0fa..9d1a75a856f823 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index 99046ee7a9a8df..cd2e3feaf71280 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_expandable_flyout.devdocs.json b/api_docs/kbn_expandable_flyout.devdocs.json index 1590d2e5ac52dd..7fcd69c291a2ec 100644 --- a/api_docs/kbn_expandable_flyout.devdocs.json +++ b/api_docs/kbn_expandable_flyout.devdocs.json @@ -80,31 +80,38 @@ "\nWrap your plugin with this context for the ExpandableFlyout React component." ], "signature": [ - "({ children }: ", + "React.ForwardRefExoticComponent<", "ExpandableFlyoutProviderProps", - ") => JSX.Element" + " & React.RefAttributes<", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.ExpandableFlyoutApi", + "text": "ExpandableFlyoutApi" + }, + ">>" ], "path": "packages/kbn-expandable-flyout/src/context.tsx", "deprecated": false, "trackAdoption": false, + "returnComment": [], "children": [ { "parentPluginId": "@kbn/expandable-flyout", "id": "def-common.ExpandableFlyoutProvider.$1", - "type": "Object", + "type": "Uncategorized", "tags": [], - "label": "{ children }", + "label": "props", "description": [], "signature": [ - "ExpandableFlyoutProviderProps" + "P" ], - "path": "packages/kbn-expandable-flyout/src/context.tsx", + "path": "node_modules/@types/react/index.d.ts", "deprecated": false, - "trackAdoption": false, - "isRequired": true + "trackAdoption": false } ], - "returnComment": [], "initialIsOpen": false }, { @@ -118,7 +125,13 @@ ], "signature": [ "() => ", - "ExpandableFlyoutContext" + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.ExpandableFlyoutContext", + "text": "ExpandableFlyoutContext" + } ], "path": "packages/kbn-expandable-flyout/src/context.tsx", "deprecated": false, @@ -129,6 +142,389 @@ } ], "interfaces": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext", + "type": "Interface", + "tags": [], + "label": "ExpandableFlyoutContext", + "description": [], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.panels", + "type": "Object", + "tags": [], + "label": "panels", + "description": [ + "\nRight, left and preview panels" + ], + "signature": [ + "State" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout", + "type": "Function", + "tags": [], + "label": "openFlyout", + "description": [ + "\nOpen the flyout with left, right and/or preview panels" + ], + "signature": [ + "(panels: { left?: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined; right?: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined; preview?: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined; }) => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout.$1", + "type": "Object", + "tags": [], + "label": "panels", + "description": [], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout.$1.left", + "type": "Object", + "tags": [], + "label": "left", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout.$1.right", + "type": "Object", + "tags": [], + "label": "right", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout.$1.preview", + "type": "Object", + "tags": [], + "label": "preview", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openRightPanel", + "type": "Function", + "tags": [], + "label": "openRightPanel", + "description": [ + "\nReplaces the current right panel with a new one" + ], + "signature": [ + "(panel: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + ") => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openRightPanel.$1", + "type": "Object", + "tags": [], + "label": "panel", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + } + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openLeftPanel", + "type": "Function", + "tags": [], + "label": "openLeftPanel", + "description": [ + "\nReplaces the current left panel with a new one" + ], + "signature": [ + "(panel: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + ") => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openLeftPanel.$1", + "type": "Object", + "tags": [], + "label": "panel", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + } + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openPreviewPanel", + "type": "Function", + "tags": [], + "label": "openPreviewPanel", + "description": [ + "\nAdd a new preview panel to the list of current preview panels" + ], + "signature": [ + "(panel: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + ") => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openPreviewPanel.$1", + "type": "Object", + "tags": [], + "label": "panel", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + } + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.closeRightPanel", + "type": "Function", + "tags": [], + "label": "closeRightPanel", + "description": [ + "\nCloses right panel" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.closeLeftPanel", + "type": "Function", + "tags": [], + "label": "closeLeftPanel", + "description": [ + "\nCloses left panel" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.closePreviewPanel", + "type": "Function", + "tags": [], + "label": "closePreviewPanel", + "description": [ + "\nCloses all preview panels" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.previousPreviewPanel", + "type": "Function", + "tags": [], + "label": "previousPreviewPanel", + "description": [ + "\nGo back to previous preview panel" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.closeFlyout", + "type": "Function", + "tags": [], + "label": "closeFlyout", + "description": [ + "\nClose all panels and closes flyout" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/expandable-flyout", "id": "def-common.ExpandableFlyoutProps", @@ -267,7 +663,57 @@ } ], "enums": [], - "misc": [], - "objects": [] + "misc": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutApi", + "type": "Type", + "tags": [], + "label": "ExpandableFlyoutApi", + "description": [], + "signature": [ + "Pick<", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.ExpandableFlyoutContext", + "text": "ExpandableFlyoutContext" + }, + ", \"openFlyout\"> & { getState: () => ", + "State", + "; }" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext", + "type": "Object", + "tags": [], + "label": "ExpandableFlyoutContext", + "description": [], + "signature": [ + "React.Context<", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.ExpandableFlyoutContext", + "text": "ExpandableFlyoutContext" + }, + " | undefined>" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ] } } \ No newline at end of file diff --git a/api_docs/kbn_expandable_flyout.mdx b/api_docs/kbn_expandable_flyout.mdx index 86a0743c821f5f..d6f3aa5be6f6b1 100644 --- a/api_docs/kbn_expandable_flyout.mdx +++ b/api_docs/kbn_expandable_flyout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-expandable-flyout title: "@kbn/expandable-flyout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/expandable-flyout plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/expandable-flyout'] --- import kbnExpandableFlyoutObj from './kbn_expandable_flyout.devdocs.json'; @@ -21,13 +21,19 @@ Contact [@elastic/security-threat-hunting-investigations](https://github.com/org | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 13 | 0 | 4 | 3 | +| 33 | 0 | 13 | 3 | ## Common +### Objects + + ### Functions ### Interfaces +### Consts, variables and types + + diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index d0b208fb97a8eb..4d30f7635f93fb 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 46afc78d5608ef..602d4b76e4b693 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 7ae2b45126f93b..512b1b3bf1c1c1 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index 7d52e4ad2721e6..c0b9ff1ab00b0b 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_generate_csv.mdx b/api_docs/kbn_generate_csv.mdx index bf0035738a250e..3927471f085142 100644 --- a/api_docs/kbn_generate_csv.mdx +++ b/api_docs/kbn_generate_csv.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv title: "@kbn/generate-csv" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv'] --- import kbnGenerateCsvObj from './kbn_generate_csv.devdocs.json'; diff --git a/api_docs/kbn_generate_csv_types.mdx b/api_docs/kbn_generate_csv_types.mdx index 62ba0711eb5117..0e35a01c9f842f 100644 --- a/api_docs/kbn_generate_csv_types.mdx +++ b/api_docs/kbn_generate_csv_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv-types title: "@kbn/generate-csv-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv-types'] --- import kbnGenerateCsvTypesObj from './kbn_generate_csv_types.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.devdocs.json b/api_docs/kbn_guided_onboarding.devdocs.json index ec142f58f5b6ca..80777a0b547649 100644 --- a/api_docs/kbn_guided_onboarding.devdocs.json +++ b/api_docs/kbn_guided_onboarding.devdocs.json @@ -271,6 +271,27 @@ "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/guided-onboarding", + "id": "def-common.GuideState.params", + "type": "Object", + "tags": [], + "label": "params", + "description": [], + "signature": [ + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideParams", + "text": "GuideParams" + }, + " | undefined" + ], + "path": "packages/kbn-guided-onboarding/src/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -294,7 +315,7 @@ "label": "id", "description": [], "signature": [ - "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\"" + "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\" | \"step4\"" ], "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, @@ -336,7 +357,7 @@ "label": "id", "description": [], "signature": [ - "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\"" + "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\" | \"step4\"" ], "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, @@ -558,6 +579,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/guided-onboarding", + "id": "def-common.GuideParams", + "type": "Type", + "tags": [], + "label": "GuideParams", + "description": [], + "signature": [ + "{ [x: string]: string; }" + ], + "path": "packages/kbn-guided-onboarding/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/guided-onboarding", "id": "def-common.GuideStatus", @@ -583,7 +619,7 @@ "label": "GuideStepIds", "description": [], "signature": [ - "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\"" + "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\" | \"step4\"" ], "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, @@ -757,7 +793,7 @@ "label": "steps", "description": [], "signature": [ - "({ id: \"step1\"; title: string; descriptionList: string[]; location: { appID: string; path: string; }; integration: string; } | { id: \"step2\"; title: string; descriptionList: (string | { descriptionText: string; linkText: string; linkUrl: string; isLinkExternal: true; })[]; location: { appID: string; path: string; }; manualCompletion: { title: string; description: string; readyToCompleteOnNavigation: true; }; } | { id: \"step3\"; title: string; description: string; manualCompletion: { title: string; description: string; }; location: { appID: string; path: string; }; })[]" + "({ id: \"step1\"; title: string; descriptionList: string[]; location: { appID: string; path: string; }; integration: string; } | { id: \"step2\"; title: string; descriptionList: (string | { descriptionText: string; linkText: string; linkUrl: string; isLinkExternal: true; })[]; location: { appID: string; path: string; }; manualCompletion: { title: string; description: string; readyToCompleteOnNavigation: true; }; } | { id: \"step3\"; title: string; description: string; manualCompletion: { title: string; description: string; }; location: { appID: string; path: string; }; } | { id: \"step4\"; title: string; description: string; location: { appID: string; path: string; }; })[]" ], "path": "packages/kbn-guided-onboarding/src/common/test_guide_config.ts", "deprecated": false, diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index dd29963efcb66f..fa3960f635cfa2 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/pla | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 52 | 0 | 50 | 3 | +| 54 | 0 | 52 | 3 | ## Common diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 7f02b89475b3e2..52466ad20426f8 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index 4955b1030ea079..cd8245459b82a6 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index 26528fbcb1d05b..f6df58ece93400 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index ca6f2a8e2ad02d..65d82c8aa5eb72 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index ade2366ab0e331..139ab20896b0dd 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 0a6a407c727d54..c5d491f697f42f 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index 7bf9b5d9dd0c81..50c599f41ff0c7 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index fd1da1af109523..d6377597c1189d 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 3bfb04e709422f..4d82a84859ef64 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 503dbab8e37546..d739403a3224c4 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index dea3262170428c..479812a7adc8eb 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index aa82431ea0e3f6..66f07ded661988 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_json_ast.mdx b/api_docs/kbn_json_ast.mdx index cb2330485201b4..9ba905d6961339 100644 --- a/api_docs/kbn_json_ast.mdx +++ b/api_docs/kbn_json_ast.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-json-ast title: "@kbn/json-ast" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/json-ast plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/json-ast'] --- import kbnJsonAstObj from './kbn_json_ast.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 7954d741bfcb35..9b3835108da87e 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_language_documentation_popover.mdx b/api_docs/kbn_language_documentation_popover.mdx index 7ceb5757539a4d..34e8d2903cb26d 100644 --- a/api_docs/kbn_language_documentation_popover.mdx +++ b/api_docs/kbn_language_documentation_popover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation-popover title: "@kbn/language-documentation-popover" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation-popover plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 81a30f0763d922..6c2e32868b8c70 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index 8cd4373bdcf17a..c5fa54588432d1 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index f660720b2133cf..a0492d6bad71a4 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index 05c475dfc93c28..2cfeb2a59556cb 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index d82c025dbab1ee..0faf79405aec0e 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_date_picker.mdx b/api_docs/kbn_ml_date_picker.mdx index f92ec1a980b788..6d638b542f37fc 100644 --- a/api_docs/kbn_ml_date_picker.mdx +++ b/api_docs/kbn_ml_date_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-picker title: "@kbn/ml-date-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-picker plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-picker'] --- import kbnMlDatePickerObj from './kbn_ml_date_picker.devdocs.json'; diff --git a/api_docs/kbn_ml_error_utils.devdocs.json b/api_docs/kbn_ml_error_utils.devdocs.json new file mode 100644 index 00000000000000..8872d1df30f9a3 --- /dev/null +++ b/api_docs/kbn_ml_error_utils.devdocs.json @@ -0,0 +1,800 @@ +{ + "id": "@kbn/ml-error-utils", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLRequestFailure", + "type": "Class", + "tags": [ + "export", + "class", + "typedef", + "extends" + ], + "label": "MLRequestFailure", + "description": [ + "\nML Request Failure\n" + ], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLRequestFailure", + "text": "MLRequestFailure" + }, + " extends Error" + ], + "path": "x-pack/packages/ml/error_utils/src/request_error.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLRequestFailure.Unnamed", + "type": "Function", + "tags": [ + "constructor" + ], + "label": "Constructor", + "description": [ + "\nCreates an instance of MLRequestFailure.\n" + ], + "signature": [ + "any" + ], + "path": "x-pack/packages/ml/error_utils/src/request_error.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLRequestFailure.Unnamed.$1", + "type": "Object", + "tags": [], + "label": "error", + "description": [], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLErrorObject", + "text": "MLErrorObject" + } + ], + "path": "x-pack/packages/ml/error_utils/src/request_error.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLRequestFailure.Unnamed.$2", + "type": "CompoundType", + "tags": [], + "label": "resp", + "description": [], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + } + ], + "path": "x-pack/packages/ml/error_utils/src/request_error.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.extractErrorMessage", + "type": "Function", + "tags": [], + "label": "extractErrorMessage", + "description": [ + "\nExtract only the error message within the response error\ncoming from Kibana, Elasticsearch, and our own ML messages.\n" + ], + "signature": [ + "(error: ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + }, + ") => string" + ], + "path": "x-pack/packages/ml/error_utils/src/process_errors.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.extractErrorMessage.$1", + "type": "CompoundType", + "tags": [], + "label": "error", + "description": [], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + } + ], + "path": "x-pack/packages/ml/error_utils/src/process_errors.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.extractErrorProperties", + "type": "Function", + "tags": [], + "label": "extractErrorProperties", + "description": [ + "\nExtract properties of the error object from within the response error\ncoming from Kibana, Elasticsearch, and our own ML messages.\n" + ], + "signature": [ + "(error: ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + }, + ") => ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLErrorObject", + "text": "MLErrorObject" + } + ], + "path": "x-pack/packages/ml/error_utils/src/process_errors.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.extractErrorProperties.$1", + "type": "CompoundType", + "tags": [], + "label": "error", + "description": [], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + } + ], + "path": "x-pack/packages/ml/error_utils/src/process_errors.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isBoomError", + "type": "Function", + "tags": [ + "export" + ], + "label": "isBoomError", + "description": [ + "\nType guard to check if error is of type Boom." + ], + "signature": [ + "(error: unknown) => boolean" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isBoomError.$1", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isErrorString", + "type": "Function", + "tags": [ + "export" + ], + "label": "isErrorString", + "description": [ + "\nType guard to check if error is a string." + ], + "signature": [ + "(error: unknown) => boolean" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isErrorString.$1", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isEsErrorBody", + "type": "Function", + "tags": [ + "export" + ], + "label": "isEsErrorBody", + "description": [ + "\nType guard to check if error is of type EsErrorBody" + ], + "signature": [ + "(error: unknown) => boolean" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isEsErrorBody.$1", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isMLResponseError", + "type": "Function", + "tags": [ + "export" + ], + "label": "isMLResponseError", + "description": [ + "\nType guard to check if error is of type MLResponseError." + ], + "signature": [ + "(error: unknown) => boolean" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isMLResponseError.$1", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.ErrorMessage", + "type": "Interface", + "tags": [ + "export", + "interface", + "typedef" + ], + "label": "ErrorMessage", + "description": [ + "\nInterface holding error message" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.ErrorMessage.message", + "type": "string", + "tags": [ + "type" + ], + "label": "message", + "description": [ + "\nmessage" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject", + "type": "Interface", + "tags": [ + "export", + "interface", + "typedef" + ], + "label": "MLErrorObject", + "description": [ + "\nML Error Object" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject.causedBy", + "type": "string", + "tags": [ + "type" + ], + "label": "causedBy", + "description": [ + "\nOptional causedBy" + ], + "signature": [ + "string | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject.message", + "type": "string", + "tags": [ + "type" + ], + "label": "message", + "description": [ + "\nmessage" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject.statusCode", + "type": "number", + "tags": [ + "type" + ], + "label": "statusCode", + "description": [ + "\nOptional statusCode" + ], + "signature": [ + "number | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject.fullError", + "type": "Object", + "tags": [ + "type" + ], + "label": "fullError", + "description": [ + "\nOptional fullError" + ], + "signature": [ + "ErrorResponseBase", + " | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLHttpFetchErrorBase", + "type": "Interface", + "tags": [ + "export", + "interface", + "typedef", + "template", + "extends" + ], + "label": "MLHttpFetchErrorBase", + "description": [ + "\nMLHttpFetchErrorBase" + ], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLHttpFetchErrorBase", + "text": "MLHttpFetchErrorBase" + }, + " extends ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.IHttpFetchError", + "text": "IHttpFetchError" + }, + "" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLHttpFetchErrorBase.body", + "type": "Uncategorized", + "tags": [ + "type" + ], + "label": "body", + "description": [ + "\nbody" + ], + "signature": [ + "T" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError", + "type": "Interface", + "tags": [ + "export", + "interface", + "typedef" + ], + "label": "MLResponseError", + "description": [ + "\nML Response error" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError.statusCode", + "type": "number", + "tags": [ + "type" + ], + "label": "statusCode", + "description": [ + "\nstatusCode" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError.error", + "type": "string", + "tags": [ + "type" + ], + "label": "error", + "description": [ + "\nerror" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError.message", + "type": "string", + "tags": [ + "type" + ], + "label": "message", + "description": [ + "\nmessage" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError.attributes", + "type": "Object", + "tags": [ + "type" + ], + "label": "attributes", + "description": [ + "\nOptional attributes" + ], + "signature": [ + "{ body: ", + "ErrorResponseBase", + "; } | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.QueryErrorMessage", + "type": "Interface", + "tags": [], + "label": "QueryErrorMessage", + "description": [ + "\nTo be used for client side errors related to search query bars." + ], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.QueryErrorMessage", + "text": "QueryErrorMessage" + }, + " extends ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorMessage", + "text": "ErrorMessage" + } + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.QueryErrorMessage.query", + "type": "string", + "tags": [ + "type" + ], + "label": "query", + "description": [ + "\nquery" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.ErrorType", + "type": "Type", + "tags": [ + "export", + "typedef" + ], + "label": "ErrorType", + "description": [ + "\nUnion type of error types" + ], + "signature": [ + "string | ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLHttpFetchError", + "text": "MLHttpFetchError" + }, + " | ", + "ErrorResponseBase", + " | ", + "Boom", + " | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.EsErrorBody", + "type": "Type", + "tags": [ + "typedef" + ], + "label": "EsErrorBody", + "description": [ + "\nShort hand type of estypes.ErrorResponseBase." + ], + "signature": [ + "ErrorResponseBase" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.EsErrorRootCause", + "type": "Type", + "tags": [ + "typedef" + ], + "label": "EsErrorRootCause", + "description": [ + "\nShort hand type of estypes.ErrorCause." + ], + "signature": [ + "ErrorCauseKeys", + " & { [property: string]: any; }" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLHttpFetchError", + "type": "Type", + "tags": [ + "export", + "typedef" + ], + "label": "MLHttpFetchError", + "description": [ + "\nMLHttpFetchError" + ], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLHttpFetchErrorBase", + "text": "MLHttpFetchErrorBase" + }, + "<", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLResponseError", + "text": "MLResponseError" + }, + ">" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_ml_error_utils.mdx b/api_docs/kbn_ml_error_utils.mdx new file mode 100644 index 00000000000000..39fbc8c7d9cc26 --- /dev/null +++ b/api_docs/kbn_ml_error_utils.mdx @@ -0,0 +1,39 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnMlErrorUtilsPluginApi +slug: /kibana-dev-docs/api/kbn-ml-error-utils +title: "@kbn/ml-error-utils" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/ml-error-utils plugin +date: 2023-04-24 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-error-utils'] +--- +import kbnMlErrorUtilsObj from './kbn_ml_error_utils.devdocs.json'; + + + +Contact [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 36 | 0 | 8 | 0 | + +## Common + +### Functions + + +### Classes + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_ml_is_defined.mdx b/api_docs/kbn_ml_is_defined.mdx index 9d000095378bd7..22473afe43b101 100644 --- a/api_docs/kbn_ml_is_defined.mdx +++ b/api_docs/kbn_ml_is_defined.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-defined title: "@kbn/ml-is-defined" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-defined plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-defined'] --- import kbnMlIsDefinedObj from './kbn_ml_is_defined.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index 40ce5731de31e2..6418b8eaa65f67 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_local_storage.mdx b/api_docs/kbn_ml_local_storage.mdx index f14deff209d77b..b6ab8338b1be15 100644 --- a/api_docs/kbn_ml_local_storage.mdx +++ b/api_docs/kbn_ml_local_storage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-local-storage title: "@kbn/ml-local-storage" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-local-storage plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-local-storage'] --- import kbnMlLocalStorageObj from './kbn_ml_local_storage.devdocs.json'; diff --git a/api_docs/kbn_ml_nested_property.mdx b/api_docs/kbn_ml_nested_property.mdx index ef15635229b533..20e06ed847ebbb 100644 --- a/api_docs/kbn_ml_nested_property.mdx +++ b/api_docs/kbn_ml_nested_property.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-nested-property title: "@kbn/ml-nested-property" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-nested-property plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-nested-property'] --- import kbnMlNestedPropertyObj from './kbn_ml_nested_property.devdocs.json'; diff --git a/api_docs/kbn_ml_number_utils.mdx b/api_docs/kbn_ml_number_utils.mdx index 05c5432c4f57f3..e17712d9bddd99 100644 --- a/api_docs/kbn_ml_number_utils.mdx +++ b/api_docs/kbn_ml_number_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-number-utils title: "@kbn/ml-number-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-number-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-number-utils'] --- import kbnMlNumberUtilsObj from './kbn_ml_number_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_query_utils.mdx b/api_docs/kbn_ml_query_utils.mdx index 8327a7ed0b4d55..ceef8788887602 100644 --- a/api_docs/kbn_ml_query_utils.mdx +++ b/api_docs/kbn_ml_query_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-query-utils title: "@kbn/ml-query-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-query-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-query-utils'] --- import kbnMlQueryUtilsObj from './kbn_ml_query_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_random_sampler_utils.mdx b/api_docs/kbn_ml_random_sampler_utils.mdx index 4998fe175ef710..e065a2d921874b 100644 --- a/api_docs/kbn_ml_random_sampler_utils.mdx +++ b/api_docs/kbn_ml_random_sampler_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-random-sampler-utils title: "@kbn/ml-random-sampler-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-random-sampler-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-random-sampler-utils'] --- import kbnMlRandomSamplerUtilsObj from './kbn_ml_random_sampler_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_route_utils.mdx b/api_docs/kbn_ml_route_utils.mdx index 8e1d50e0f93647..619f44aa959044 100644 --- a/api_docs/kbn_ml_route_utils.mdx +++ b/api_docs/kbn_ml_route_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-route-utils title: "@kbn/ml-route-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-route-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-route-utils'] --- import kbnMlRouteUtilsObj from './kbn_ml_route_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index f18f1327cb2561..c338805fe37b6d 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_ml_trained_models_utils.mdx b/api_docs/kbn_ml_trained_models_utils.mdx index 43aeec4bf32ca8..77df8b9fc96a93 100644 --- a/api_docs/kbn_ml_trained_models_utils.mdx +++ b/api_docs/kbn_ml_trained_models_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-trained-models-utils title: "@kbn/ml-trained-models-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-trained-models-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-trained-models-utils'] --- import kbnMlTrainedModelsUtilsObj from './kbn_ml_trained_models_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_url_state.mdx b/api_docs/kbn_ml_url_state.mdx index 390009d4685e53..271d8d798bb8bc 100644 --- a/api_docs/kbn_ml_url_state.mdx +++ b/api_docs/kbn_ml_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-url-state title: "@kbn/ml-url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-url-state plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-url-state'] --- import kbnMlUrlStateObj from './kbn_ml_url_state.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 605d07152c5b07..0d286dc40dd463 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_object_versioning.devdocs.json b/api_docs/kbn_object_versioning.devdocs.json index bb1210067600d1..551e1feda1d0b3 100644 --- a/api_docs/kbn_object_versioning.devdocs.json +++ b/api_docs/kbn_object_versioning.devdocs.json @@ -768,6 +768,28 @@ "path": "packages/kbn-object-versioning/lib/content_management_types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/object-versioning", + "id": "def-common.ServicesDefinition.mSearch", + "type": "Object", + "tags": [], + "label": "mSearch", + "description": [], + "signature": [ + "{ out?: { result?: ", + { + "pluginId": "@kbn/object-versioning", + "scope": "common", + "docId": "kibKbnObjectVersioningPluginApi", + "section": "def-common.VersionableObject", + "text": "VersionableObject" + }, + " | undefined; } | undefined; } | undefined" + ], + "path": "packages/kbn-object-versioning/lib/content_management_types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -978,6 +1000,28 @@ "path": "packages/kbn-object-versioning/lib/content_management_types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/object-versioning", + "id": "def-common.ServiceTransforms.mSearch", + "type": "Object", + "tags": [], + "label": "mSearch", + "description": [], + "signature": [ + "{ out: { result: ", + { + "pluginId": "@kbn/object-versioning", + "scope": "common", + "docId": "kibKbnObjectVersioningPluginApi", + "section": "def-common.ObjectTransforms", + "text": "ObjectTransforms" + }, + "; }; }" + ], + "path": "packages/kbn-object-versioning/lib/content_management_types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_object_versioning.mdx b/api_docs/kbn_object_versioning.mdx index 8d3135bdd7f767..bf8dc9c9b2edb0 100644 --- a/api_docs/kbn_object_versioning.mdx +++ b/api_docs/kbn_object_versioning.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-object-versioning title: "@kbn/object-versioning" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/object-versioning plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/object-versioning'] --- import kbnObjectVersioningObj from './kbn_object_versioning.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 53 | 1 | 48 | 0 | +| 55 | 1 | 50 | 0 | ## Common diff --git a/api_docs/kbn_observability_alert_details.mdx b/api_docs/kbn_observability_alert_details.mdx index 48886f59e61609..109f0b15690e79 100644 --- a/api_docs/kbn_observability_alert_details.mdx +++ b/api_docs/kbn_observability_alert_details.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alert-details title: "@kbn/observability-alert-details" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alert-details plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alert-details'] --- import kbnObservabilityAlertDetailsObj from './kbn_observability_alert_details.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 58204fde5d9802..2be790b6675bf4 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index ac5cab95f43ec5..c03883615b27e9 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index d351b9e974a462..39eafb2553e2a4 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 820b381b56e3bf..fe2a28edfb759c 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index 4f63a60e98d211..dfafc2995046a9 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 481b2546124046..90bfd0727dc864 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 5d88f2bb7cd877..894f6e1897cb77 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_file_maps.mdx b/api_docs/kbn_repo_file_maps.mdx index fb0c69e2683274..9baafe7d47198c 100644 --- a/api_docs/kbn_repo_file_maps.mdx +++ b/api_docs/kbn_repo_file_maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-file-maps title: "@kbn/repo-file-maps" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-file-maps plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-file-maps'] --- import kbnRepoFileMapsObj from './kbn_repo_file_maps.devdocs.json'; diff --git a/api_docs/kbn_repo_linter.mdx b/api_docs/kbn_repo_linter.mdx index fa756525922ae9..0dcb15d07b2b06 100644 --- a/api_docs/kbn_repo_linter.mdx +++ b/api_docs/kbn_repo_linter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-linter title: "@kbn/repo-linter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-linter plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-linter'] --- import kbnRepoLinterObj from './kbn_repo_linter.devdocs.json'; diff --git a/api_docs/kbn_repo_path.mdx b/api_docs/kbn_repo_path.mdx index dc04d8563bc528..44df1cb8c7ba89 100644 --- a/api_docs/kbn_repo_path.mdx +++ b/api_docs/kbn_repo_path.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-path title: "@kbn/repo-path" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-path plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-path'] --- import kbnRepoPathObj from './kbn_repo_path.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index af0847941070f8..3725fe5986da20 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_reporting_common.mdx b/api_docs/kbn_reporting_common.mdx index 52ff2a0ac41b53..660528756fcdf5 100644 --- a/api_docs/kbn_reporting_common.mdx +++ b/api_docs/kbn_reporting_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-common title: "@kbn/reporting-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-common'] --- import kbnReportingCommonObj from './kbn_reporting_common.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index 5f654ffa57ef81..ee1fd9df0086e1 100644 --- a/api_docs/kbn_rison.mdx +++ b/api_docs/kbn_rison.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rison title: "@kbn/rison" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rison plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index 16eb8b268e12af..56b40b98c61113 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_saved_objects_settings.mdx b/api_docs/kbn_saved_objects_settings.mdx index 2d5a7a078d24c6..ac3326c810bdff 100644 --- a/api_docs/kbn_saved_objects_settings.mdx +++ b/api_docs/kbn_saved_objects_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-saved-objects-settings title: "@kbn/saved-objects-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/saved-objects-settings plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/saved-objects-settings'] --- import kbnSavedObjectsSettingsObj from './kbn_saved_objects_settings.devdocs.json'; diff --git a/api_docs/kbn_security_solution_side_nav.mdx b/api_docs/kbn_security_solution_side_nav.mdx index 476ecb69cdbcc4..dbd27615a28866 100644 --- a/api_docs/kbn_security_solution_side_nav.mdx +++ b/api_docs/kbn_security_solution_side_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-side-nav title: "@kbn/security-solution-side-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-side-nav plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-side-nav'] --- import kbnSecuritySolutionSideNavObj from './kbn_security_solution_side_nav.devdocs.json'; diff --git a/api_docs/kbn_security_solution_storybook_config.mdx b/api_docs/kbn_security_solution_storybook_config.mdx index 5bec888bb4e5a4..31bc6c9d9cbc97 100644 --- a/api_docs/kbn_security_solution_storybook_config.mdx +++ b/api_docs/kbn_security_solution_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-storybook-config title: "@kbn/security-solution-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-storybook-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-storybook-config'] --- import kbnSecuritySolutionStorybookConfigObj from './kbn_security_solution_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 7d0c70dbb7b392..f51ac2903abc58 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_data_table.mdx b/api_docs/kbn_securitysolution_data_table.mdx index 1c5b3548f92c49..cbb7736cb66be4 100644 --- a/api_docs/kbn_securitysolution_data_table.mdx +++ b/api_docs/kbn_securitysolution_data_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-data-table title: "@kbn/securitysolution-data-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-data-table plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-data-table'] --- import kbnSecuritysolutionDataTableObj from './kbn_securitysolution_data_table.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_ecs.mdx b/api_docs/kbn_securitysolution_ecs.mdx index 085a9a78a69cfa..2d549e62753a9d 100644 --- a/api_docs/kbn_securitysolution_ecs.mdx +++ b/api_docs/kbn_securitysolution_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-ecs title: "@kbn/securitysolution-ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-ecs plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-ecs'] --- import kbnSecuritysolutionEcsObj from './kbn_securitysolution_ecs.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 1df63d48779346..966df1d43affcf 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index 35453fb0b1a31f..ef163cd54d784d 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_grouping.mdx b/api_docs/kbn_securitysolution_grouping.mdx index 7276dcef35bd3b..548914f17feb8e 100644 --- a/api_docs/kbn_securitysolution_grouping.mdx +++ b/api_docs/kbn_securitysolution_grouping.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-grouping title: "@kbn/securitysolution-grouping" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-grouping plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-grouping'] --- import kbnSecuritysolutionGroupingObj from './kbn_securitysolution_grouping.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index a7cb93e0ca41c5..012e37a5de10c8 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json b/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json index 59129f93d4ed59..9450fd73b66712 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json @@ -303,7 +303,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alerts_filter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; }" + "; } & { uuid?: string | undefined; alerts_filter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; }" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -326,7 +326,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alerts_filter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; })[]" + "; } & { uuid?: string | undefined; alerts_filter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; })[]" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -349,7 +349,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alertsFilter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; })[]" + "; } & { uuid?: string | undefined; alertsFilter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; })[]" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -372,13 +372,30 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alertsFilter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; }" + "; } & { uuid?: string | undefined; alertsFilter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; }" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionFrequency", + "type": "Type", + "tags": [], + "label": "RuleActionFrequency", + "description": [ + "\nThe action frequency defines when the action runs (for example, only on rule execution or at specific time intervals)." + ], + "signature": [ + "{ summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; }" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionGroup", @@ -409,6 +426,23 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionNotifyWhen", + "type": "Type", + "tags": [], + "label": "RuleActionNotifyWhen", + "description": [ + "\nThe condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`" + ], + "signature": [ + "\"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionParams", @@ -434,6 +468,23 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionSummary", + "type": "Type", + "tags": [], + "label": "RuleActionSummary", + "description": [ + "\nAction summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert" + ], + "signature": [ + "boolean" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionThrottle", @@ -1013,7 +1064,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alerts_filter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; })[], ({ group: string; id: string; action_type_id: string; params: ", + "; } & { uuid?: string | undefined; alerts_filter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; })[], ({ group: string; id: string; action_type_id: string; params: ", { "pluginId": "@kbn/securitysolution-io-ts-alerting-types", "scope": "common", @@ -1021,7 +1072,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alerts_filter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; })[] | undefined, unknown>" + "; } & { uuid?: string | undefined; alerts_filter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; })[] | undefined, unknown>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts", "deprecated": false, @@ -1570,13 +1621,11 @@ "<{ uuid: ", "Type", "; alerts_filter: ", - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1585,14 +1634,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -1625,7 +1720,31 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>; }>]>>" + "; }>>; }>>]>; }>; frequency: ", + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>; }>]>>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -1640,13 +1759,11 @@ "label": "RuleActionAlertsFilter", "description": [], "signature": [ - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1655,14 +1772,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -1695,7 +1858,7 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>" + "; }>>; }>>]>; }>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -1746,13 +1909,11 @@ "<{ uuid: ", "Type", "; alerts_filter: ", - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1761,14 +1922,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -1801,7 +2008,31 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>; }>]>>>" + "; }>>; }>>]>; }>; frequency: ", + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>; }>]>>>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -1852,13 +2083,11 @@ "<{ uuid: ", "Type", "; alertsFilter: ", - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1867,14 +2096,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -1907,7 +2182,31 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>; }>]>>>" + "; }>>; }>>]>; }>; frequency: ", + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>; }>]>>>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -1956,13 +2255,11 @@ "<{ uuid: ", "Type", "; alertsFilter: ", - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1971,14 +2268,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -2011,13 +2354,75 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>; }>]>>" + "; }>>; }>>]>; }>; frequency: ", + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>; }>]>>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionFrequency", + "type": "Object", + "tags": [], + "label": "RuleActionFrequency", + "description": [], + "signature": [ + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionGroup", @@ -2048,6 +2453,28 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionNotifyWhen", + "type": "Object", + "tags": [], + "label": "RuleActionNotifyWhen", + "description": [], + "signature": [ + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionParams", @@ -2080,6 +2507,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionSummary", + "type": "Object", + "tags": [], + "label": "RuleActionSummary", + "description": [], + "signature": [ + "BooleanC" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionThrottle", diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 1a5fe7a8eff636..c19977b427358a 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution-platform](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 141 | 0 | 122 | 0 | +| 147 | 0 | 125 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.devdocs.json b/api_docs/kbn_securitysolution_io_ts_list_types.devdocs.json index cbd27335f2f354..44e52b99ed0312 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.devdocs.json +++ b/api_docs/kbn_securitysolution_io_ts_list_types.devdocs.json @@ -106,7 +106,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -148,7 +154,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -204,7 +216,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -260,7 +278,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -327,7 +351,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -439,7 +469,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1015,6 +1051,102 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps", + "type": "Interface", + "tags": [], + "label": "ApiListDuplicateProps", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.ApiListDuplicateProps", + "text": "ApiListDuplicateProps" + }, + " extends Omit<", + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.DuplicateExceptionListProps", + "text": "DuplicateExceptionListProps" + }, + ", \"http\" | \"signal\">" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps.onError", + "type": "Function", + "tags": [], + "label": "onError", + "description": [], + "signature": [ + "(err: Error) => void" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps.onError.$1", + "type": "Object", + "tags": [], + "label": "err", + "description": [], + "signature": [ + "Error" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps.onSuccess", + "type": "Function", + "tags": [], + "label": "onSuccess", + "description": [], + "signature": [ + "(newList: { _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; }) => void" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps.onSuccess.$1", + "type": "Object", + "tags": [], + "label": "newList", + "description": [], + "signature": [ + "{ _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; }" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.ApiListExportProps", @@ -1140,6 +1272,66 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListProps", + "type": "Interface", + "tags": [], + "label": "DuplicateExceptionListProps", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.DuplicateExceptionListProps", + "text": "DuplicateExceptionListProps" + }, + " extends BaseParams" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListProps.listId", + "type": "string", + "tags": [], + "label": "listId", + "description": [], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListProps.namespaceType", + "type": "CompoundType", + "tags": [], + "label": "namespaceType", + "description": [], + "signature": [ + "\"single\" | \"agnostic\"" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListProps.includeExpiredExceptions", + "type": "boolean", + "tags": [], + "label": "includeExpiredExceptions", + "description": [], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.ExceptionFilterResponse", @@ -1393,7 +1585,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1538,7 +1736,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1597,7 +1801,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1759,7 +1969,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1855,7 +2071,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1911,7 +2133,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -2015,7 +2243,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -2270,7 +2504,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -2293,12 +2533,18 @@ { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.UseExceptionListsProps.notifications", - "type": "Any", + "type": "Object", "tags": [], "label": "notifications", "description": [], "signature": [ - "any" + { + "pluginId": "@kbn/core-notifications-browser", + "scope": "common", + "docId": "kibKbnCoreNotificationsBrowserPluginApi", + "section": "def-common.NotificationsStart", + "text": "NotificationsStart" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -3081,6 +3327,36 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListQuerySchema", + "type": "Type", + "tags": [], + "label": "DuplicateExceptionListQuerySchema", + "description": [], + "signature": [ + "{ list_id: string; namespace_type: \"single\" | \"agnostic\" | undefined; include_expired_exceptions: \"true\" | \"false\" | undefined; }" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListQuerySchemaDecoded", + "type": "Type", + "tags": [], + "label": "DuplicateExceptionListQuerySchemaDecoded", + "description": [], + "signature": [ + "Omit<{ list_id: string; namespace_type: \"single\" | \"agnostic\"; include_expired_exceptions: \"true\" | \"false\" | undefined; }, \"namespace_type\"> & { namespace_type: \"single\" | \"agnostic\"; }" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.EndpointEntriesArray", @@ -6256,6 +6532,34 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.duplicateExceptionListQuerySchema", + "type": "Object", + "tags": [], + "label": "duplicateExceptionListQuerySchema", + "description": [], + "signature": [ + "ExactC", + "<", + "TypeC", + "<{ list_id: ", + "Type", + "; namespace_type: ", + "Type", + "<\"single\" | \"agnostic\", \"single\" | \"agnostic\" | undefined, unknown>; include_expired_exceptions: ", + "UnionC", + "<[", + "KeyofC", + "<{ true: null; false: null; }>, ", + "UndefinedC", + "]>; }>>" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.endpointEntriesArray", diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index fae1f7cdd2313d..61c51d7af0af1a 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution-platform](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 516 | 1 | 503 | 0 | +| 528 | 0 | 515 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 360fd192007550..97859ef4b0ff62 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 2b6f02cc7215be..087b27374d68be 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.devdocs.json b/api_docs/kbn_securitysolution_list_api.devdocs.json index b4c5635b5b281d..1a07636db6652e 100644 --- a/api_docs/kbn_securitysolution_list_api.devdocs.json +++ b/api_docs/kbn_securitysolution_list_api.devdocs.json @@ -348,6 +348,57 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-list-api", + "id": "def-common.duplicateExceptionList", + "type": "Function", + "tags": [ + "throws" + ], + "label": "duplicateExceptionList", + "description": [ + "\nDuplicate an ExceptionList and its items by providing a ExceptionList list_id\n" + ], + "signature": [ + "({ http, includeExpiredExceptions, listId, namespaceType, signal, }: ", + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.DuplicateExceptionListProps", + "text": "DuplicateExceptionListProps" + }, + ") => Promise<{ _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; }>" + ], + "path": "packages/kbn-securitysolution-list-api/src/api/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-list-api", + "id": "def-common.duplicateExceptionList.$1", + "type": "Object", + "tags": [], + "label": "{\n http,\n includeExpiredExceptions,\n listId,\n namespaceType,\n signal,\n}", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.DuplicateExceptionListProps", + "text": "DuplicateExceptionListProps" + } + ], + "path": "packages/kbn-securitysolution-list-api/src/api/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-list-api", "id": "def-common.exportExceptionList", diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 0255ba2088e5ed..aadca05334b923 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution-platform](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 67 | 0 | 64 | 0 | +| 69 | 0 | 65 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 02b65ca6fde372..2d19d6111ef538 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.devdocs.json b/api_docs/kbn_securitysolution_list_hooks.devdocs.json index a1741a54232f05..14c2ee5a4b1224 100644 --- a/api_docs/kbn_securitysolution_list_hooks.devdocs.json +++ b/api_docs/kbn_securitysolution_list_hooks.devdocs.json @@ -255,7 +255,15 @@ "label": "useApi", "description": [], "signature": [ - "(http: HttpStart) => ", + "(http: ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + }, + ") => ", { "pluginId": "@kbn/securitysolution-list-hooks", "scope": "common", @@ -276,7 +284,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-list-hooks/src/use_api/index.ts", "deprecated": false, @@ -1023,6 +1037,52 @@ ], "returnComment": [] }, + { + "parentPluginId": "@kbn/securitysolution-list-hooks", + "id": "def-common.ExceptionsApi.duplicateExceptionList", + "type": "Function", + "tags": [], + "label": "duplicateExceptionList", + "description": [], + "signature": [ + "(arg: ", + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.ApiListDuplicateProps", + "text": "ApiListDuplicateProps" + }, + ") => Promise" + ], + "path": "packages/kbn-securitysolution-list-hooks/src/use_api/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-list-hooks", + "id": "def-common.ExceptionsApi.duplicateExceptionList.$1", + "type": "Object", + "tags": [], + "label": "arg", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.ApiListDuplicateProps", + "text": "ApiListDuplicateProps" + } + ], + "path": "packages/kbn-securitysolution-list-hooks/src/use_api/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, { "parentPluginId": "@kbn/securitysolution-list-hooks", "id": "def-common.ExceptionsApi.getExceptionItem", diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index 786b822ce39def..e5f63ce69a7917 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution-platform](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 60 | 0 | 47 | 0 | +| 62 | 0 | 49 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index 7f1e021d2997e5..9fbc74f2281fae 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index 7b8ea7f8fa1532..d99c895cfbb075 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index bbaae18e81926a..fc1a7126299da7 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index b22479ca8cb62b..5b42ba79461cd3 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index e959b8f8857f1b..c538168844e590 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index 4dce3d0bcc3547..7c066060f57b37 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index b3241f80861cf5..57b4b0757c666f 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index 7fdc62d57e0e5b..0e0a7de0598540 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index 74b168d876714a..cbe1bbc5921053 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index c67019c0244471..fd754396fe38d4 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index 523f937ac03089..704bf0d55a5885 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index 7e026fc462e170..d5cbbf14400083 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index 6744cc7da9aa8b..6e187d6a67d6d7 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index 604622664685df..f1eafa0073b758 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index 2218b967c0672a..345346e831748c 100644 --- a/api_docs/kbn_shared_ux_file_context.mdx +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-context title: "@kbn/shared-ux-file-context" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-context plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] --- import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index 961468e71949a5..e242bd651fa0da 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index f82ac77b98773c..4e391b9d8ab271 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx index 1ab3bafb4ee5fb..662c634769dd1a 100644 --- a/api_docs/kbn_shared_ux_file_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks title: "@kbn/shared-ux-file-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] --- import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_picker.mdx b/api_docs/kbn_shared_ux_file_picker.mdx index f48a321a34b549..1e22cbdd34b006 100644 --- a/api_docs/kbn_shared_ux_file_picker.mdx +++ b/api_docs/kbn_shared_ux_file_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-picker title: "@kbn/shared-ux-file-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-picker plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-picker'] --- import kbnSharedUxFilePickerObj from './kbn_shared_ux_file_picker.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_types.mdx b/api_docs/kbn_shared_ux_file_types.mdx index cf77561da9e6e1..43c10d7423ef71 100644 --- a/api_docs/kbn_shared_ux_file_types.mdx +++ b/api_docs/kbn_shared_ux_file_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-types title: "@kbn/shared-ux-file-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-types'] --- import kbnSharedUxFileTypesObj from './kbn_shared_ux_file_types.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_upload.mdx b/api_docs/kbn_shared_ux_file_upload.mdx index 84e8d32366c106..7a684a3ddf9b69 100644 --- a/api_docs/kbn_shared_ux_file_upload.mdx +++ b/api_docs/kbn_shared_ux_file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-upload title: "@kbn/shared-ux-file-upload" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-upload plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-upload'] --- import kbnSharedUxFileUploadObj from './kbn_shared_ux_file_upload.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index d79e2ec066afd6..7a12cb245836a9 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index 35763b229e4001..dc82b967602d2c 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index 714f376ffb7c5d..18b157f0504187 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index f4f56efd304857..8d43b2bc2609dc 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index 5d46068526cc83..6ad8500d4ffa1f 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index a703893f75dec8..da16a3294719e7 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index c6146d86359c0c..a79ab025d8f394 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index 2fc62d3203dab0..675d8ca547baa5 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index e4a702e9d4de6a..87063d4968ca9e 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index 0281befb594edf..d83b54deb377fa 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index bc4addd3208ad1..a915fe7ce8d2fc 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index 0144374aab4fbf..d386725c0f96e4 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index da46bad1d78e54..b609f61949e3a9 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index 4681ccfbf18d88..3b8a6aea768c4d 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index ac7fbb18cee1d1..b549d3be8ce485 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index c5f2135a7e18e2..57b7f77917dbfb 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index 90f8c67503a8a0..cde75adfcbd03a 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index 0c4bc9ae73323c..449ec14d0e89b0 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx index da5e9ae1eb9a10..54e8a60fb3a71f 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.mdx +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found title: "@kbn/shared-ux-prompt-not-found" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-not-found plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] --- import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index aa816570472a83..64431ebf0a9f65 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index 135cdbf742f311..8f903a6a4599b8 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 9ca85a30f64b68..aa8aeb5ed628b6 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index ce99698fd9f1be..1f594a6b275037 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index 0e8f337108e828..35030e8f65b692 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_slo_schema.mdx b/api_docs/kbn_slo_schema.mdx index 97a606deb5c99f..b86313f3a585f7 100644 --- a/api_docs/kbn_slo_schema.mdx +++ b/api_docs/kbn_slo_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-slo-schema title: "@kbn/slo-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/slo-schema plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/slo-schema'] --- import kbnSloSchemaObj from './kbn_slo_schema.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index d1a8eebc6a3526..8a4c14deb442e3 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index 0217dc9a965db5..8a33d6033f83b7 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index b4b67a76d63452..8bb33b6f9fc6c9 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index fd6b8221cc8fd3..0dcbe8b65314aa 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index a2a8c3a870b1a8..009ae2d00a260c 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 245f8573362f47..077c288dbbd069 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 7c8dcb3038c282..ac6e29488cd2d1 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index f4a6b85e91f0c1..4b38e19d2c1445 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index 2da057aa80b43e..1e093e1c87a673 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx index efa271163a5279..ec139fac154dcb 100644 --- a/api_docs/kbn_ts_projects.mdx +++ b/api_docs/kbn_ts_projects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ts-projects title: "@kbn/ts-projects" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ts-projects plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ts-projects'] --- import kbnTsProjectsObj from './kbn_ts_projects.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index e36f3796a6a55a..c335bc8053866e 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; diff --git a/api_docs/kbn_ui_actions_browser.mdx b/api_docs/kbn_ui_actions_browser.mdx index 3cd1a6aef2f772..e4f48183850088 100644 --- a/api_docs/kbn_ui_actions_browser.mdx +++ b/api_docs/kbn_ui_actions_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-actions-browser title: "@kbn/ui-actions-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-actions-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-actions-browser'] --- import kbnUiActionsBrowserObj from './kbn_ui_actions_browser.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index 11107207bbef64..cfc3a2c905dbb9 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index a6c35e39c2b54a..2306fe2c1523ed 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_url_state.devdocs.json b/api_docs/kbn_url_state.devdocs.json new file mode 100644 index 00000000000000..e7659b142aa8e7 --- /dev/null +++ b/api_docs/kbn_url_state.devdocs.json @@ -0,0 +1,99 @@ +{ + "id": "@kbn/url-state", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/url-state", + "id": "def-common.useSyncToUrl", + "type": "Function", + "tags": [], + "label": "useSyncToUrl", + "description": [ + "\nSync any object with browser query string using @knb/rison" + ], + "signature": [ + "(key: string, restore: (data: TValueToSerialize) => void, cleanupOnHistoryNavigation?: boolean) => (valueToSerialize?: TValueToSerialize | undefined) => void" + ], + "path": "packages/kbn-url-state/use_sync_to_url.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/url-state", + "id": "def-common.useSyncToUrl.$1", + "type": "string", + "tags": [], + "label": "key", + "description": [ + "query string param to use" + ], + "signature": [ + "string" + ], + "path": "packages/kbn-url-state/use_sync_to_url.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/url-state", + "id": "def-common.useSyncToUrl.$2", + "type": "Function", + "tags": [], + "label": "restore", + "description": [ + "use this to handle restored state" + ], + "signature": [ + "(data: TValueToSerialize) => void" + ], + "path": "packages/kbn-url-state/use_sync_to_url.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/url-state", + "id": "def-common.useSyncToUrl.$3", + "type": "boolean", + "tags": [], + "label": "cleanupOnHistoryNavigation", + "description": [ + "use history events to cleanup state on back / forward naviation. true by default" + ], + "signature": [ + "boolean" + ], + "path": "packages/kbn-url-state/use_sync_to_url.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_url_state.mdx b/api_docs/kbn_url_state.mdx new file mode 100644 index 00000000000000..1ecdc4cf5f6b89 --- /dev/null +++ b/api_docs/kbn_url_state.mdx @@ -0,0 +1,30 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnUrlStatePluginApi +slug: /kibana-dev-docs/api/kbn-url-state +title: "@kbn/url-state" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/url-state plugin +date: 2023-04-24 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/url-state'] +--- +import kbnUrlStateObj from './kbn_url_state.devdocs.json'; + + + +Contact [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 0 | 0 | + +## Common + +### Functions + + diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index 99b0de88ef5ef1..9678a8f3d941ec 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index d7c2e09558b574..0847fe990eba30 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index 18348076c4a98a..bd7b6dc7e7ca05 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index a743985adbe807..3bb37ce9fb475e 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index 73c55462f3c811..89bec78fbe1d08 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index e20a39ed1d8b51..35b6ab39c77a03 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index cb370c5b41c005..277ae838dc455c 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index e4252f4bb1d238..9710a9b016f88a 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index f13e3882eba311..e9b23fb4642c82 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 0a8741d0767722..7e35673a3ef08e 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index b9b65d3c94bc68..18e54115992df5 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index e3bb7df0269459..f791025b3e3532 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 704afaaf19d4de..348e30ab776d89 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.devdocs.json b/api_docs/lists.devdocs.json index cf0f553770aee6..a5d1cf263468f6 100644 --- a/api_docs/lists.devdocs.json +++ b/api_docs/lists.devdocs.json @@ -736,7 +736,7 @@ "\nCreate the Trusted Apps Agnostic list if it does not yet exist (`null` is returned if it does exist)" ], "signature": [ - "({ listId, namespaceType, }: ", + "({ list, namespaceType, includeExpiredExceptions, }: ", "DuplicateExceptionListOptions", ") => Promise<{ _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; } | null>" ], @@ -749,7 +749,7 @@ "id": "def-server.ExceptionListClient.duplicateExceptionListAndItems.$1", "type": "Object", "tags": [], - "label": "{\n listId,\n namespaceType,\n }", + "label": "{\n list,\n namespaceType,\n includeExpiredExceptions,\n }", "description": [], "signature": [ "DuplicateExceptionListOptions" diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 6a3f788c87be37..7a4f5df515fb3d 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index f2a3ee2fecf346..e86efc700e7eaf 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index 3a3a5d8d917d17..d5ef557d051167 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index 4205c9b472f0be..97034dddf8ac65 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.devdocs.json b/api_docs/ml.devdocs.json index a235055918f577..a714fc22ec0c9a 100644 --- a/api_docs/ml.devdocs.json +++ b/api_docs/ml.devdocs.json @@ -3306,41 +3306,6 @@ "returnComment": [], "initialIsOpen": false }, - { - "parentPluginId": "ml", - "id": "def-common.extractErrorMessage", - "type": "Function", - "tags": [], - "label": "extractErrorMessage", - "description": [], - "signature": [ - "(error: ", - "ErrorType", - ") => string" - ], - "path": "x-pack/plugins/ml/common/util/errors/process_errors.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "ml", - "id": "def-common.extractErrorMessage.$1", - "type": "CompoundType", - "tags": [], - "label": "error", - "description": [], - "signature": [ - "ErrorType" - ], - "path": "x-pack/plugins/ml/common/util/errors/process_errors.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "ml", "id": "def-common.getDefaultCapabilities", diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index d7fab7ce5ee29b..d977d474923842 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) for questi | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 259 | 9 | 83 | 40 | +| 257 | 9 | 81 | 39 | ## Client diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index fbb5d4166d7c01..90c474ac78f1ab 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 390050d8aca212..8d7ae64cb77bab 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index c1da45703e1e0b..59a2eae887fb53 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index ea9412b022b551..f1ec49c987768b 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index 45300a85184d2e..bf6aade55bc14f 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index caed04a1ba9d53..01cac0e1d423ae 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; diff --git a/api_docs/observability_shared.mdx b/api_docs/observability_shared.mdx index 85db14ed31dc3a..321906e5ba6ff5 100644 --- a/api_docs/observability_shared.mdx +++ b/api_docs/observability_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityShared title: "observabilityShared" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityShared plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityShared'] --- import observabilitySharedObj from './observability_shared.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 57b293ee7b2e3f..171effe7c54674 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index dc3d7eab17a064..e0ee93073348ba 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,13 +15,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 595 | 491 | 37 | +| 597 | 493 | 37 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 69056 | 526 | 59639 | 1335 | +| 69151 | 525 | 59683 | 1333 | ## Plugin Directory @@ -30,7 +30,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 259 | 8 | 254 | 26 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 36 | 1 | 32 | 2 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 39 | 0 | 24 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 603 | 1 | 582 | 42 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 606 | 1 | 585 | 42 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 43 | 0 | 43 | 110 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | Asset manager plugin for entity assets (inventory, topology, etc) | 3 | 0 | 3 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 9 | 0 | 9 | 0 | @@ -48,7 +48,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | cloudLinks | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | Adds the links to the Elastic Cloud console | 0 | 0 | 0 | 0 | | | [@elastic/kibana-cloud-security-posture](https://github.com/orgs/elastic/teams/kibana-cloud-security-posture) | The cloud security posture plugin | 17 | 0 | 2 | 2 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 13 | 0 | 13 | 1 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Content management app | 143 | 0 | 124 | 7 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Content management app | 149 | 0 | 126 | 6 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 301 | 0 | 294 | 13 | | crossClusterReplication | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | | customBranding | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Enables customization of Kibana | 0 | 0 | 0 | 0 | @@ -99,7 +99,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | globalSearchProviders | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 0 | 0 | 0 | 0 | | graph | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 0 | 0 | 0 | 0 | | grokdebugger | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | -| | [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | Guided onboarding framework | 56 | 0 | 55 | 0 | +| | [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | Guided onboarding framework | 57 | 0 | 56 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 143 | 0 | 104 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Image embeddable | 3 | 0 | 3 | 1 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 4 | 0 | 4 | 0 | @@ -123,7 +123,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 41 | 0 | 41 | 6 | | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | - | 269 | 0 | 268 | 29 | | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | - | 67 | 0 | 67 | 0 | -| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the machine learning features provided by Elastic. | 259 | 9 | 83 | 40 | +| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the machine learning features provided by Elastic. | 257 | 9 | 81 | 39 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | - | 15 | 3 | 13 | 1 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | - | 9 | 0 | 9 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 34 | 0 | 34 | 2 | @@ -167,7 +167,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 257 | 1 | 214 | 20 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [@elastic/kibana-localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 540 | 10 | 511 | 49 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 542 | 10 | 513 | 49 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Adds UI Actions service to Kibana | 134 | 2 | 92 | 9 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Extends UI Actions plugin with more functionality | 206 | 0 | 140 | 9 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 296 | 0 | 270 | 6 | @@ -214,7 +214,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 18 | 0 | 2 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 27 | 0 | 27 | 3 | -| | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 154 | 0 | 154 | 17 | +| | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 153 | 0 | 153 | 17 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 11 | 0 | 11 | 0 | | | [@elastic/kibana-qa](https://github.com/orgs/elastic/teams/kibana-qa) | - | 12 | 0 | 12 | 0 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 19 | 0 | 17 | 0 | @@ -408,14 +408,14 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 255 | 1 | 197 | 15 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 12 | 0 | 12 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 1 | 0 | -| | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 13 | 0 | 4 | 3 | +| | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 33 | 0 | 13 | 3 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 20 | 0 | 16 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 0 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 29 | 0 | 29 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 1 | 0 | 0 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 10 | 0 | 10 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 10 | 0 | 10 | 0 | -| | [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | - | 52 | 0 | 50 | 3 | +| | [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | - | 54 | 0 | 52 | 3 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 33 | 3 | 24 | 6 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 3 | 0 | 3 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 1 | 0 | 1 | 0 | @@ -437,6 +437,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | - | 534 | 1 | 1 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 93 | 2 | 61 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 52 | 0 | 4 | 0 | +| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 36 | 0 | 8 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 2 | 0 | 0 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 3 | 0 | 2 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 5 | 0 | 3 | 0 | @@ -449,7 +450,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 8 | 1 | 8 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 31 | 1 | 24 | 1 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 71 | 0 | 69 | 3 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 53 | 1 | 48 | 0 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 55 | 1 | 50 | 0 | | | [@elastic/actionable-observability](https://github.com/orgs/elastic/teams/actionable-observability) | - | 7 | 0 | 7 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 45 | 0 | 45 | 10 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 51 | 5 | 34 | 0 | @@ -475,13 +476,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 104 | 0 | 93 | 1 | | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 20 | 0 | 15 | 4 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 15 | 0 | 7 | 0 | -| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 141 | 0 | 122 | 0 | -| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 516 | 1 | 503 | 0 | +| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 147 | 0 | 125 | 0 | +| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 528 | 0 | 515 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 65 | 0 | 36 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 28 | 0 | 21 | 0 | -| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 67 | 0 | 64 | 0 | +| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 69 | 0 | 65 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 35 | 0 | 23 | 0 | -| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 60 | 0 | 47 | 0 | +| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 62 | 0 | 49 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 210 | 10 | 163 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 26 | 0 | 23 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 120 | 0 | 116 | 0 | @@ -542,6 +543,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 18 | 0 | 8 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 45 | 0 | 36 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 7 | 0 | 6 | 0 | +| | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 4 | 0 | 0 | 0 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 58 | 0 | 5 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 0 | 15 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 2 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index bd5fa373355d3f..bb5dd22da4a5de 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index 94ec36d0e142d2..fa7c514839f1d4 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 4d93f65d4f588f..052f465958e43c 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index dc655be6ea3b1b..b3e643f23c8247 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index 22004d47158636..3ad92d06484ef4 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 7af25a71322594..2230c7e475fbf3 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index bf014f1590fc24..fbb641e9ec07bf 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index 1ce68779192c78..4775f7bab3e251 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index ba213be4dd0b6e..c5cc15bc31edd1 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index b3d06632c17570..8ed4d9ce7ea8b3 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 28ee6a6fdb1e9c..28d4dfa912c949 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 50a27cc71b42c0..c4e422b6d4bf5f 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index 2f0a64f88e254a..b41919c5f029f1 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index b87efc25bb18e9..eee83610aebe7c 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index 179ea1d1861aa7..8b105e90157cad 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index b93687377e8a62..6a3ef50e5d725e 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index d193ee3ae36cfe..49cd868cf6aee3 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 863bb87c48c268..89f21abe997f4a 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 2bb9768f311c2b..41725af018c9eb 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 44b56f58ba474e..582bfce7f3253d 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 563274dd2f7508..2467dff1cd4645 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index bfd22efb5d897d..f2fd4e7fff1d3a 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index 7426f4bf3c2e2e..12ffc30623e60f 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index 07663807996de5..ec588948cbe188 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index 507775a14db43e..439a2431b43764 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index b0d73ccf969377..a445edd5a138f2 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 3870cdba63b351..29ee2d61784382 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index 27c23a0eb88db9..fbe7face22441d 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index b54750be2d6066..5ca7b1d27e7d42 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index acb04c0a79a888..41df96ec361dd0 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index a5cb1bf5bb0f13..e0a7c8eca1a70f 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.devdocs.json b/api_docs/triggers_actions_ui.devdocs.json index 8b881a6bf30053..ae29d4e3e787ab 100644 --- a/api_docs/triggers_actions_ui.devdocs.json +++ b/api_docs/triggers_actions_ui.devdocs.json @@ -2831,6 +2831,20 @@ "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertStatus.maintenanceWindowIds", + "type": "Array", + "tags": [], + "label": "maintenanceWindowIds", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -3045,6 +3059,17 @@ "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertSummary.revision", + "type": "number", + "tags": [], + "label": "revision", + "description": [], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index fc92b4f9ff6155..0dfa7fa50555b6 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 540 | 10 | 511 | 49 | +| 542 | 10 | 513 | 49 | ## Client diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 2fe2650841250b..ba54dfe3328c83 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index a46c6ed8f86f6e..0111328be34a0c 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_field_list.mdx b/api_docs/unified_field_list.mdx index 604ae78db8a3e8..97fceac316e4da 100644 --- a/api_docs/unified_field_list.mdx +++ b/api_docs/unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedFieldList title: "unifiedFieldList" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedFieldList plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index 33f08e0514e8c7..e0acc17411f179 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; diff --git a/api_docs/unified_search.devdocs.json b/api_docs/unified_search.devdocs.json index 05f2215a9a9133..8f697ba358c0c3 100644 --- a/api_docs/unified_search.devdocs.json +++ b/api_docs/unified_search.devdocs.json @@ -487,7 +487,7 @@ "OnSaveTextLanguageQueryProps", ") => void) | undefined; showSubmitButton?: boolean | undefined; submitButtonStyle?: \"full\" | \"auto\" | \"iconOnly\" | undefined; suggestionsSize?: ", "SuggestionsListSize", - " | undefined; isScreenshotMode?: boolean | undefined; onFiltersUpdated?: ((filters: ", + " | undefined; isScreenshotMode?: boolean | undefined; submitOnBlur?: boolean | undefined; onFiltersUpdated?: ((filters: ", { "pluginId": "@kbn/es-query", "scope": "common", diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 5c23136a98a7ad..9655a20a1dda9d 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index 1b5beadfecf137..f70714d1e5938b 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index b6d4f177afce29..fb8870f2325d26 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 919ee295028988..569cd4f8baf097 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index de9da5f50c4dda..71a2488190eb83 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index d17bcc483e1c31..aa3747f8476f0d 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index d73b04c29be18e..24c99b2aa19f8e 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index dfab14fdd94de4..d7649559452e86 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index 096f49cdb15413..c493a75a570760 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index e36aeeed77738d..b954ce0b719d21 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index eba3c262b3db47..b98b43d726106f 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 504196c391a348..7a80a887ba186b 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index bc9c59bd6a49ca..b760568fac4e3f 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index b8ac18365cd3ce..27174661c88453 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index 409866a3315d43..f64e0c6adbde73 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index 348c6c015d3481..a8c2abb735484b 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; From 67927e12b2723fb8360478c99c2f9c00a7ae4c0a Mon Sep 17 00:00:00 2001 From: Ido Cohen <90558359+CohenIdo@users.noreply.github.com> Date: Mon, 24 Apr 2023 10:26:39 +0300 Subject: [PATCH 29/65] [Cloud Security] update cloud security telemetry interface --- .../server/lib/telemetry/collectors/types.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts index 8651a3f577c1de..fe24101dfa8d4b 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BaseCspSetupBothPolicy } from '../../../../common/types'; +import { CspStatusCode } from '../../../../common/types'; export interface CspmUsage { indices: CspmIndicesStats; @@ -14,6 +14,12 @@ export interface CspmUsage { rules_stats: CspmRulesStats[]; } +export interface PackageSetupStatus { + status: CspStatusCode; + installedPackagePolicies: number; + healthyAgents: number; +} + export interface CspmIndicesStats { findings: IndexStats | {}; latest_findings: IndexStats | {}; @@ -21,9 +27,9 @@ export interface CspmIndicesStats { latest_vulnerabilities: IndexStats | {}; score: IndexStats | {}; latestPackageVersion: string; - cspm: BaseCspSetupBothPolicy; - kspm: BaseCspSetupBothPolicy; - vuln_mgmt: BaseCspSetupBothPolicy; + cspm: PackageSetupStatus; + kspm: PackageSetupStatus; + vuln_mgmt: PackageSetupStatus; } export interface IndexStats { From 03464e79c22149e072216b51afec5b7ccd4f3faf Mon Sep 17 00:00:00 2001 From: Navarone Feekery <13634519+navarone-feekery@users.noreply.github.com> Date: Mon, 24 Apr 2023 10:17:00 +0200 Subject: [PATCH 30/65] [Enterprise Search] Clean up rich configurable fields UI (#155282) - Add spacing between `EuiRow` and `EuiPanel` for `ConnectorConfigurationField`. - Hide fields that have any `ui_restrictions` - Fix labelling for sensitive textareas - Other misc cleanup tasks --- .../common/connectors/native_connectors.ts | 14 +++++ .../common/types/connectors.ts | 2 +- .../__mocks__/search_indices.mock.ts | 2 + .../__mocks__/view_index.mock.ts | 2 + .../connector_configuration_field.tsx | 9 ++- .../connector_configuration_form.tsx | 55 +++++++++++++------ .../connector_configuration_logic.test.ts | 23 ++++++++ .../connector_configuration_logic.ts | 4 ++ 8 files changed, 92 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/connectors/native_connectors.ts b/x-pack/plugins/enterprise_search/common/connectors/native_connectors.ts index 8d962ee206f9ee..78a720304d9966 100644 --- a/x-pack/plugins/enterprise_search/common/connectors/native_connectors.ts +++ b/x-pack/plugins/enterprise_search/common/connectors/native_connectors.ts @@ -27,6 +27,7 @@ export const NATIVE_CONNECTOR_DEFINITIONS: Record + +

{label}

+ + } + > {textarea}
) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx index 2ef04c8e2c18bc..7627c5c869469d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx @@ -17,6 +17,7 @@ import { EuiForm, EuiFormRow, EuiPanel, + EuiSpacer, EuiToolTip, } from '@elastic/eui'; @@ -48,6 +49,12 @@ export const ConnectorConfigurationForm = () => { {} ); + const filteredConfigView = localConfigView.filter( + (configEntry) => + configEntry.ui_restrictions.length <= 0 && + dependenciesSatisfied(configEntry.depends_on, dependencyLookup) + ); + return ( { @@ -56,17 +63,16 @@ export const ConnectorConfigurationForm = () => { }} component="form" > - {localConfigView.map((configEntry) => { + {filteredConfigView.map((configEntry, index) => { const { default_value: defaultValue, depends_on: dependencies, key, display, label, + sensitive, tooltip, } = configEntry; - // toggle label goes next to the element, not in the row - const hasDependencies = dependencies.length > 0; const helpText = defaultValue ? i18n.translate( 'xpack.enterpriseSearch.content.indices.configurationConnector.config.defaultValue', @@ -76,26 +82,41 @@ export const ConnectorConfigurationForm = () => { } ) : ''; + // toggle and sensitive textarea labels go next to the element, not in the row const rowLabel = - display !== DisplayType.TOGGLE ? ( + display === DisplayType.TOGGLE || (display === DisplayType.TEXTAREA && sensitive) ? ( + <> + ) : (

{label}

- ) : ( - <> ); - return hasDependencies ? ( - dependenciesSatisfied(dependencies, dependencyLookup) ? ( - - - - - - ) : ( - <> - ) - ) : ( + if (dependencies.length > 0) { + // dynamic spacing without CSS + const previousField = filteredConfigView[index - 1]; + const nextField = filteredConfigView[index + 1]; + + const topSpacing = + !previousField || previousField.depends_on.length <= 0 ? : <>; + + const bottomSpacing = + !nextField || nextField.depends_on.length <= 0 ? : <>; + + return ( + <> + {topSpacing} + + + + + + {bottomSpacing} + + ); + } + + return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts index 9ff1fc60db1688..64e7d1af9c9994 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts @@ -61,6 +61,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'oldBar', }, }, @@ -79,6 +80,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'oldBar', }, }, @@ -94,6 +96,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'oldBar', }, ], @@ -111,6 +114,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, }); @@ -127,6 +131,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, }, @@ -142,6 +147,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, ], @@ -160,6 +166,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'foofoo', }, password: { @@ -172,6 +179,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, }); @@ -186,6 +194,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'foofoo', }, password: { @@ -198,6 +207,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, }); @@ -212,6 +222,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fafa', }); expect(ConnectorConfigurationLogic.values).toEqual({ @@ -227,6 +238,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'foofoo', }, password: { @@ -239,6 +251,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, }, @@ -254,6 +267,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'foofoo', }, { @@ -267,6 +281,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, ], @@ -281,6 +296,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fafa', }, password: { @@ -293,6 +309,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, }, @@ -308,6 +325,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fafa', }, { @@ -321,6 +339,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, ], @@ -345,6 +364,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, ], @@ -381,6 +401,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, ], @@ -402,6 +423,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, ], @@ -424,6 +446,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'Barbara', }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts index 02ebb7888b7b89..84b0fd4d23fdb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts @@ -66,6 +66,7 @@ export interface ConfigEntry { required: boolean; sensitive: boolean; tooltip: string; + ui_restrictions: string[]; value: string | number | boolean | null; } @@ -245,6 +246,8 @@ export const ConnectorConfigurationLogic = kea< required, sensitive, tooltip, + // eslint-disable-next-line @typescript-eslint/naming-convention + ui_restrictions, value, } ) => ({ @@ -259,6 +262,7 @@ export const ConnectorConfigurationLogic = kea< required, sensitive, tooltip, + ui_restrictions, value, }, }), From 5f24c14d583613e070d0da739f300d6bc8406901 Mon Sep 17 00:00:00 2001 From: Alexander Wert Date: Mon, 24 Apr 2023 10:59:11 +0200 Subject: [PATCH 31/65] Added OTel information to service metadata icons (#154458) ## Summary Added OpenTelemetry information to metadata icons on OTel services: image --- x-pack/plugins/apm/common/agent_name.ts | 12 +++- .../shared/agent_icon/get_agent_icon.ts | 2 +- .../shared/agent_icon/icons/otel_default.svg | 12 ++++ .../components/shared/service_icons/index.tsx | 28 +++++++- .../shared/service_icons/otel_details.tsx | 64 +++++++++++++++++++ .../services/get_service_metadata_details.ts | 45 ++++++++++--- .../services/get_service_metadata_icons.ts | 6 ++ .../apm/server/routes/services/route.ts | 11 +--- 8 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/agent_icon/icons/otel_default.svg create mode 100644 x-pack/plugins/apm/public/components/shared/service_icons/otel_details.tsx diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 21cecfcf348f7b..7782cc044e950d 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { AgentName } from '../typings/es_schemas/ui/fields/agent'; +import { + AgentName, + OpenTelemetryAgentName, +} from '../typings/es_schemas/ui/fields/agent'; import { ServerlessType } from './serverless'; /* @@ -47,8 +50,11 @@ export const AGENT_NAMES: AgentName[] = [ ...OPEN_TELEMETRY_AGENT_NAMES, ]; -export const isOpenTelemetryAgentName = (agentName: AgentName) => - OPEN_TELEMETRY_AGENT_NAMES.includes(agentName); +export function isOpenTelemetryAgentName( + agentName: string +): agentName is OpenTelemetryAgentName { + return OPEN_TELEMETRY_AGENT_NAMES.includes(agentName as AgentName); +} export const JAVA_AGENT_NAMES: AgentName[] = ['java', 'opentelemetry/java']; diff --git a/x-pack/plugins/apm/public/components/shared/agent_icon/get_agent_icon.ts b/x-pack/plugins/apm/public/components/shared/agent_icon/get_agent_icon.ts index 9a775df78c4e32..04bc276dcfa65c 100644 --- a/x-pack/plugins/apm/public/components/shared/agent_icon/get_agent_icon.ts +++ b/x-pack/plugins/apm/public/components/shared/agent_icon/get_agent_icon.ts @@ -25,7 +25,7 @@ import darkIosIcon from './icons/ios_dark.svg'; import javaIcon from './icons/java.svg'; import nodeJsIcon from './icons/nodejs.svg'; import ocamlIcon from './icons/ocaml.svg'; -import openTelemetryIcon from './icons/opentelemetry.svg'; +import openTelemetryIcon from './icons/otel_default.svg'; import phpIcon from './icons/php.svg'; import pythonIcon from './icons/python.svg'; import rubyIcon from './icons/ruby.svg'; diff --git a/x-pack/plugins/apm/public/components/shared/agent_icon/icons/otel_default.svg b/x-pack/plugins/apm/public/components/shared/agent_icon/icons/otel_default.svg new file mode 100644 index 00000000000000..ef13d7190c4f9a --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/agent_icon/icons/otel_default.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx index ca9142668a65a9..f5a647d3ca4880 100644 --- a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx @@ -16,9 +16,12 @@ import { getServerlessIcon } from '../agent_icon/get_serverless_icon'; import { CloudDetails } from './cloud_details'; import { ServerlessDetails } from './serverless_details'; import { ContainerDetails } from './container_details'; +import { OTelDetails } from './otel_details'; import { IconPopover } from './icon_popover'; import { ServiceDetails } from './service_details'; import { ServerlessType } from '../../../../common/serverless'; +import { isOpenTelemetryAgentName } from '../../../../common/agent_name'; +import openTelemetryIcon from '../agent_icon/icons/opentelemetry.svg'; interface Props { serviceName: string; @@ -70,7 +73,13 @@ export function getContainerIcon(container?: ContainerType) { } } -type Icons = 'service' | 'container' | 'serverless' | 'cloud' | 'alerts'; +type Icons = + | 'service' + | 'opentelemetry' + | 'container' + | 'serverless' + | 'cloud' + | 'alerts'; export interface PopoverItem { key: Icons; @@ -142,6 +151,23 @@ export function ServiceIcons({ start, end, serviceName }: Props) { }), component: , }, + { + key: 'opentelemetry', + icon: { + type: openTelemetryIcon, + }, + isVisible: + !!icons?.agentName && isOpenTelemetryAgentName(icons.agentName), + title: i18n.translate('xpack.apm.serviceIcons.opentelemetry', { + defaultMessage: 'OpenTelemetry', + }), + component: ( + + ), + }, { key: 'container', icon: { diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/otel_details.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/otel_details.tsx new file mode 100644 index 00000000000000..a9d2e8a963cac5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/service_icons/otel_details.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDescriptionList, EuiDescriptionListProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { APIReturnType } from '../../../services/rest/create_call_apm_api'; + +type ServiceDetailsReturnType = + APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>; + +interface Props { + opentelemetry: ServiceDetailsReturnType['opentelemetry']; + agentName?: string; +} + +export function OTelDetails({ opentelemetry }: Props) { + if (!opentelemetry) { + return null; + } + + const listItems: EuiDescriptionListProps['listItems'] = []; + listItems.push({ + title: i18n.translate( + 'xpack.apm.serviceIcons.otelDetails.opentelemetry.language', + { + defaultMessage: 'Language', + } + ), + description: ( + <>{!!opentelemetry.language ? opentelemetry.language : 'unknown'} + ), + }); + + if (!!opentelemetry.sdkVersion) { + listItems.push({ + title: i18n.translate( + 'xpack.apm.serviceIcons.otelDetails.opentelemetry.sdkVersion', + { + defaultMessage: 'OTel SDK version', + } + ), + description: <>{opentelemetry.sdkVersion}, + }); + } + + if (!!opentelemetry.autoVersion) { + listItems.push({ + title: i18n.translate( + 'xpack.apm.serviceIcons.otelDetails.opentelemetry.autoVersion', + { + defaultMessage: 'Auto instrumentation agent version', + } + ), + description: <>{opentelemetry.autoVersion}, + }); + } + + return ; +} diff --git a/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts b/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts index 4563bd851f8a80..2231caa7df71ef 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts @@ -24,17 +24,18 @@ import { SERVICE_VERSION, FAAS_ID, FAAS_TRIGGER_TYPE, + LABEL_TELEMETRY_AUTO_VERSION, } from '../../../common/es_fields/apm'; import { ContainerType } from '../../../common/service_metadata'; import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; -import { getProcessorEventForTransactions } from '../../lib/helpers/transactions'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { should } from './get_service_metadata_icons'; +import { isOpenTelemetryAgentName } from '../../../common/agent_name'; type ServiceMetadataDetailsRaw = Pick< TransactionRaw, - 'service' | 'agent' | 'host' | 'container' | 'kubernetes' | 'cloud' + 'service' | 'agent' | 'host' | 'container' | 'kubernetes' | 'cloud' | 'labels' >; export interface ServiceMetadataDetails { @@ -50,6 +51,11 @@ export interface ServiceMetadataDetails { version: string; }; }; + opentelemetry?: { + language?: string; + sdkVersion?: string; + autoVersion?: string; + }; container?: { ids?: string[]; image?: string; @@ -81,13 +87,11 @@ export interface ServiceMetadataDetails { export async function getServiceMetadataDetails({ serviceName, apmEventClient, - searchAggregatedTransactions, start, end, }: { serviceName: string; apmEventClient: APMEventClient; - searchAggregatedTransactions: boolean; start: number; end: number; }): Promise { @@ -99,16 +103,27 @@ export async function getServiceMetadataDetails({ const params = { apm: { events: [ - getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.transaction, ProcessorEvent.error, ProcessorEvent.metric, ], }, - sort: [{ '@timestamp': { order: 'desc' as const } }], + sort: [ + { _score: { order: 'desc' as const } }, + { '@timestamp': { order: 'desc' as const } }, + ], body: { track_total_hits: 1, size: 1, - _source: [SERVICE, AGENT, HOST, CONTAINER, KUBERNETES, CLOUD], + _source: [ + SERVICE, + AGENT, + HOST, + CONTAINER, + KUBERNETES, + CLOUD, + LABEL_TELEMETRY_AUTO_VERSION, + ], query: { bool: { filter, should } }, aggs: { serviceVersions: { @@ -178,8 +193,8 @@ export async function getServiceMetadataDetails({ }; } - const { service, agent, host, kubernetes, container, cloud } = response.hits - .hits[0]._source as ServiceMetadataDetailsRaw; + const { service, agent, host, kubernetes, container, cloud, labels } = + response.hits.hits[0]._source as ServiceMetadataDetailsRaw; const serviceMetadataDetails = { versions: response.aggregations?.serviceVersions.buckets.map( @@ -190,6 +205,17 @@ export async function getServiceMetadataDetails({ agent, }; + const otelDetails = + !!agent?.name && isOpenTelemetryAgentName(agent.name) + ? { + language: agent.name.startsWith('opentelemetry') + ? agent.name.replace(/^opentelemetry\//, '') + : undefined, + sdkVersion: agent?.version, + autoVersion: labels?.telemetry_auto_version as string, + } + : undefined; + const totalNumberInstances = response.aggregations?.totalNumberInstances.value; @@ -238,6 +264,7 @@ export async function getServiceMetadataDetails({ return { service: serviceMetadataDetails, + opentelemetry: otelDetails, container: containerDetails, serverless: serverlessDetails, cloud: cloudDetails, diff --git a/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts b/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts index 4890648a27ad38..37214b362e1253 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts @@ -16,6 +16,9 @@ import { SERVICE_NAME, KUBERNETES_POD_NAME, HOST_OS_PLATFORM, + LABEL_TELEMETRY_AUTO_VERSION, + AGENT_VERSION, + SERVICE_FRAMEWORK_NAME, } from '../../../common/es_fields/apm'; import { ContainerType } from '../../../common/service_metadata'; import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; @@ -44,6 +47,9 @@ export const should = [ { exists: { field: CLOUD_PROVIDER } }, { exists: { field: HOST_OS_PLATFORM } }, { exists: { field: AGENT_NAME } }, + { exists: { field: AGENT_VERSION } }, + { exists: { field: SERVICE_FRAMEWORK_NAME } }, + { exists: { field: LABEL_TELEMETRY_AUTO_VERSION } }, ]; export async function getServiceMetadataIcons({ diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts index 2b31be70ad6bab..9cddb4d3577f65 100644 --- a/x-pack/plugins/apm/server/routes/services/route.ts +++ b/x-pack/plugins/apm/server/routes/services/route.ts @@ -240,22 +240,13 @@ const serviceMetadataDetailsRoute = createApmServerRoute({ handler: async (resources): Promise => { const apmEventClient = await getApmEventClient(resources); const infraMetricsClient = createInfraMetricsClient(resources); - const { params, config } = resources; + const { params } = resources; const { serviceName } = params.path; const { start, end } = params.query; - const searchAggregatedTransactions = await getSearchTransactionsEvents({ - apmEventClient, - config, - start, - end, - kuery: '', - }); - const serviceMetadataDetails = await getServiceMetadataDetails({ serviceName, apmEventClient, - searchAggregatedTransactions, start, end, }); From 84e6e36d0bf75cebfddaacd6892fd21f36e13e05 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Mon, 24 Apr 2023 11:44:19 +0200 Subject: [PATCH 32/65] [Security Solution] Add rule snooze settings on the rule details page (#155407) **Addresses:** https://github.com/elastic/kibana/issues/155406 ## Summary This PR adds rule snoozing support on the Rule Details page. https://user-images.githubusercontent.com/3775283/233387056-47a29066-f2af-4bbe-ad4f-f1002b216d7e.mov ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../components/rule_snooze_badge.tsx | 67 +++++++++++++++++++ .../rule_details_snooze_settings/index.tsx | 35 ++++++++++ .../translations.ts | 2 +- .../pages/rule_details/index.test.tsx | 5 ++ .../pages/rule_details/index.tsx | 40 ++++++----- .../components/rule_snooze_badge.tsx | 59 ---------------- .../__mocks__/rules_table_context.tsx | 1 + .../rules_table/rules_table_context.tsx | 15 ++++- .../components/rules_table/translations.ts | 7 ++ .../components/rules_table/use_columns.tsx | 25 ++++++- 10 files changed, 176 insertions(+), 80 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx rename x-pack/plugins/security_solution/public/detection_engine/{rule_management/components => rule_details_ui/pages/rule_details/components/rule_details_snooze_settings}/translations.ts (81%) delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge.tsx diff --git a/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx new file mode 100644 index 00000000000000..7fa16826eec60f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { useUserData } from '../../detections/components/user_info'; +import { hasUserCRUDPermission } from '../../common/utils/privileges'; +import { useKibana } from '../../common/lib/kibana'; +import type { RuleSnoozeSettings } from '../rule_management/logic'; +import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../rule_management/api/hooks/use_fetch_rules_snooze_settings'; + +interface RuleSnoozeBadgeProps { + /** + * Rule's snooze settings, when set to `undefined` considered as a loading state + */ + snoozeSettings: RuleSnoozeSettings | undefined; + /** + * It should represent a user readable error message happened during data snooze settings fetching + */ + error?: string; + showTooltipInline?: boolean; +} + +export function RuleSnoozeBadge({ + snoozeSettings, + error, + showTooltipInline = false, +}: RuleSnoozeBadgeProps): JSX.Element { + const RulesListNotifyBadge = useKibana().services.triggersActionsUi.getRulesListNotifyBadge; + const [{ canUserCRUD }] = useUserData(); + const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD); + const invalidateFetchRuleSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); + const isLoading = !snoozeSettings; + const rule = useMemo(() => { + return { + id: snoozeSettings?.id ?? '', + muteAll: snoozeSettings?.mute_all ?? false, + activeSnoozes: snoozeSettings?.active_snoozes ?? [], + isSnoozedUntil: snoozeSettings?.is_snoozed_until + ? new Date(snoozeSettings.is_snoozed_until) + : undefined, + snoozeSchedule: snoozeSettings?.snooze_schedule, + isEditable: hasCRUDPermissions, + }; + }, [snoozeSettings, hasCRUDPermissions]); + + if (error) { + return ( + + + + ); + } + + return ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx new file mode 100644 index 00000000000000..e610715d676ce8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useFetchRulesSnoozeSettings } from '../../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings'; +import { RuleSnoozeBadge } from '../../../../../components/rule_snooze_badge'; +import * as i18n from './translations'; + +interface RuleDetailsSnoozeBadge { + /** + * Rule's SO id (not ruleId) + */ + id: string; +} + +export function RuleDetailsSnoozeSettings({ id }: RuleDetailsSnoozeBadge): JSX.Element { + const { data: rulesSnoozeSettings, isFetching, isError } = useFetchRulesSnoozeSettings([id]); + const snoozeSettings = rulesSnoozeSettings?.[0]; + + return ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts similarity index 81% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts index 1b98a9c6212eb0..37b3b6c75ba6e0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; export const UNABLE_TO_FETCH_RULE_SNOOZE_SETTINGS = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleManagement.ruleSnoozeBadge.error.unableToFetch', + 'xpack.securitySolution.detectionEngine.ruleDetails.rulesSnoozeSettings.error.unableToFetch', { defaultMessage: 'Unable to fetch snooze settings', } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx index 28ed5a658558d1..07cbd4294cb22c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx @@ -86,6 +86,11 @@ jest.mock('react-router-dom', () => { }; }); +// RuleDetailsSnoozeSettings is an isolated component and not essential for existing tests +jest.mock('./components/rule_details_snooze_settings', () => ({ + RuleDetailsSnoozeSettings: () => <>, +})); + const mockRedirectLegacyUrl = jest.fn(); const mockGetLegacyUrlConflict = jest.fn(); jest.mock('../../../../common/lib/kibana', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index e53b23d16a46d6..6e1b4fddbd167f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -140,6 +140,7 @@ import { EditRuleSettingButtonLink } from '../../../../detections/pages/detectio import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs'; import { useBulkDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/use_bulk_duplicate_confirmation'; import { BulkActionDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation'; +import { RuleDetailsSnoozeSettings } from './components/rule_details_snooze_settings'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -539,23 +540,30 @@ const RuleDetailsPageComponent: React.FC = ({ const lastExecutionMessage = lastExecution?.message ?? ''; const ruleStatusInfo = useMemo(() => { - return ruleLoading ? ( - - - - ) : ( - - - + return ( + <> + {ruleLoading ? ( + + + + ) : ( + + + + )} + + + + ); - }, [lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); + }, [ruleId, lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); const ruleError = useMemo(() => { return ruleLoading ? ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge.tsx deleted file mode 100644 index f2a06cd5475e0d..00000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_snooze_badge.tsx +++ /dev/null @@ -1,59 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { useUserData } from '../../../detections/components/user_info'; -import { hasUserCRUDPermission } from '../../../common/utils/privileges'; -import { useKibana } from '../../../common/lib/kibana'; -import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../api/hooks/use_fetch_rules_snooze_settings'; -import { useRulesTableContext } from '../../rule_management_ui/components/rules_table/rules_table/rules_table_context'; -import * as i18n from './translations'; - -interface RuleSnoozeBadgeProps { - id: string; // Rule SO's id (not ruleId) -} - -export function RuleSnoozeBadge({ id }: RuleSnoozeBadgeProps): JSX.Element { - const RulesListNotifyBadge = useKibana().services.triggersActionsUi.getRulesListNotifyBadge; - const [{ canUserCRUD }] = useUserData(); - const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD); - const { - state: { rulesSnoozeSettings }, - } = useRulesTableContext(); - const invalidateFetchRuleSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); - const rule = useMemo(() => { - const ruleSnoozeSettings = rulesSnoozeSettings.data[id]; - - return { - id: ruleSnoozeSettings?.id ?? '', - muteAll: ruleSnoozeSettings?.mute_all ?? false, - activeSnoozes: ruleSnoozeSettings?.active_snoozes ?? [], - isSnoozedUntil: ruleSnoozeSettings?.is_snoozed_until - ? new Date(ruleSnoozeSettings.is_snoozed_until) - : undefined, - snoozeSchedule: ruleSnoozeSettings?.snooze_schedule, - isEditable: hasCRUDPermissions, - }; - }, [id, rulesSnoozeSettings, hasCRUDPermissions]); - - if (rulesSnoozeSettings.isError) { - return ( - - - - ); - } - - return ( - - ); -} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx index 8ab84b2e60a605..9ee763899eab70 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx @@ -15,6 +15,7 @@ export const useRulesTableContextMock = { rulesSnoozeSettings: { data: {}, isLoading: false, + isFetching: false, isError: false, }, pagination: { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx index fa2c64f01d2d46..938174d0c567d4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx @@ -40,8 +40,18 @@ import { RuleSource } from './rules_table_saved_state'; import { useRulesTableSavedState } from './use_rules_table_saved_state'; interface RulesSnoozeSettings { - data: Record; // The key is a rule SO's id (not ruleId) + /** + * A map object using rule SO's id (not ruleId) as keys and snooze settings as values + */ + data: Record; + /** + * Sets to true during the first data loading + */ isLoading: boolean; + /** + * Sets to true during data loading + */ + isFetching: boolean; isError: boolean; } @@ -290,6 +300,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide const { data: rulesSnoozeSettings, isLoading: isSnoozeSettingsLoading, + isFetching: isSnoozeSettingsFetching, isError: isSnoozeSettingsFetchError, refetch: refetchSnoozeSettings, } = useFetchRulesSnoozeSettings( @@ -349,6 +360,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide rulesSnoozeSettings: { data: rulesSnoozeSettingsMap, isLoading: isSnoozeSettingsLoading, + isFetching: isSnoozeSettingsFetching, isError: isSnoozeSettingsFetchError, }, pagination: { @@ -382,6 +394,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide rules, rulesSnoozeSettings, isSnoozeSettingsLoading, + isSnoozeSettingsFetching, isSnoozeSettingsFetchError, page, perPage, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts index 52b4a5d4ba6220..ad3cd896040306 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts @@ -21,3 +21,10 @@ export const ML_RULE_JOBS_WARNING_BUTTON_LABEL = i18n.translate( defaultMessage: 'Visit rule details page to investigate', } ); + +export const UNABLE_TO_FETCH_RULES_SNOOZE_SETTINGS = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.rulesSnoozeSettings.error.unableToFetch', + { + defaultMessage: 'Unable to fetch snooze settings', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx index e20a2f2c70e4f2..0ffb0ac7574a62 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx @@ -22,7 +22,7 @@ import type { } from '../../../../../common/detection_engine/rule_monitoring'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; -import { RuleSnoozeBadge } from '../../../rule_management/components/rule_snooze_badge'; +import { RuleSnoozeBadge } from '../../../components/rule_snooze_badge'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; import { SecuritySolutionLinkAnchor } from '../../../../common/components/links'; import { getRuleDetailsTabUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; @@ -46,6 +46,7 @@ import { useHasActionsPrivileges } from './use_has_actions_privileges'; import { useHasMlPermissions } from './use_has_ml_permissions'; import { useRulesTableActions } from './use_rules_table_actions'; import { MlRuleWarningPopover } from './ml_rule_warning_popover'; +import * as rulesTableI18n from './translations'; export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; @@ -108,15 +109,33 @@ const useEnabledColumn = ({ hasCRUDPermissions, startMlJobs }: ColumnsProps): Ta }; const useRuleSnoozeColumn = (): TableColumn => { + const { + state: { rulesSnoozeSettings }, + } = useRulesTableContext(); + return useMemo( () => ({ field: 'snooze', name: i18n.COLUMN_SNOOZE, - render: (_, rule: Rule) => , + render: (_, rule: Rule) => { + const snoozeSettings = rulesSnoozeSettings.data[rule.id]; + const { isFetching, isError } = rulesSnoozeSettings; + + return ( + + ); + }, width: '100px', sortable: false, }), - [] + [rulesSnoozeSettings] ); }; From f6e037794abc69271688f0bb241c3876a0b0e50d Mon Sep 17 00:00:00 2001 From: Abdul Wahab Zahid Date: Mon, 24 Apr 2023 11:46:48 +0200 Subject: [PATCH 33/65] [Synthetics] Remove "Beta" labels in Synthetics. (#155589) ## Summary Removes "Beta" labels and tooltips form the following locations: - From Kibana navigation - From Observability navigation - From Single page and Multi Step Browser Monitor Type - "Tech preview" label from Monitor Script Upload --- .../monitor_add_edit/fields/source_field.tsx | 59 ++++--------------- .../monitor_add_edit/form/field_config.tsx | 4 +- .../monitors_page/management/labels.ts | 8 --- .../page_header/monitors_page_header.tsx | 9 +-- x-pack/plugins/synthetics/public/plugin.ts | 7 +-- .../translations/translations/fr-FR.json | 4 -- .../translations/translations/ja-JP.json | 4 -- .../translations/translations/zh-CN.json | 4 -- 8 files changed, 15 insertions(+), 84 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx index f5b9602923dc4c..bd5c46810d202c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx @@ -5,16 +5,9 @@ * 2.0. */ import React, { useEffect, useState } from 'react'; -import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { - EuiTabbedContent, - EuiFormRow, - EuiBetaBadge, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiTabbedContent, EuiFormRow } from '@elastic/eui'; import { CodeEditor } from './code_editor'; import { ScriptRecorderFields } from './script_recorder_fields'; import { ConfigKey, MonacoEditorLangId } from '../types'; @@ -51,40 +44,16 @@ export const SourceField = ({ onChange, onBlur, value, isEditFlow = false }: Sou const allTabs = [ { id: 'syntheticsBrowserScriptRecorderConfig', - name: ( - - - {isEditFlow ? ( - - ) : ( - - )} - - - - - + name: isEditFlow ? ( + + ) : ( + ), 'data-test-subj': 'syntheticsSourceTab__scriptRecorder', content: ( @@ -171,9 +140,3 @@ export const SourceField = ({ onChange, onBlur, value, isEditFlow = false }: Sou /> ); }; - -const StyledBetaBadgeWrapper = styled(EuiFlexItem)` - .euiToolTipAnchor { - display: flex; - } -`; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx index 0980ef1aa961dd..b2839da207ff8b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx @@ -118,7 +118,7 @@ export const MONITOR_TYPE_CONFIG = { ), link: '#', icon: 'videoPlayer', - beta: true, + beta: false, }, [FormMonitorType.SINGLE]: { id: 'syntheticsMonitorTypeSingle', @@ -142,7 +142,7 @@ export const MONITOR_TYPE_CONFIG = { ), link: '#', icon: 'videoPlayer', - beta: true, + beta: false, }, [FormMonitorType.HTTP]: { id: 'syntheticsMonitorTypeHTTP', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts index d7f3f892c0c76b..aca280e74fcb23 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts @@ -64,14 +64,6 @@ export const ERROR_HEADING_LABEL = i18n.translate( } ); -export const BETA_TOOLTIP_MESSAGE = i18n.translate( - 'xpack.synthetics.monitors.management.betaLabel', - { - defaultMessage: - 'This functionality is in beta and is subject to change. The design and code is less mature than official generally available features and is being provided as-is with no warranties. Beta features are not subject to the support service level agreement of official generally available features.', - } -); - export const SUMMARY_LABEL = i18n.translate('xpack.synthetics.monitorManagement.summary.heading', { defaultMessage: 'Summary', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx index 8fd9f969d8e985..d4f30cb75236cf 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx @@ -7,19 +7,12 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -import { BETA_TOOLTIP_MESSAGE } from '../labels'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; export const MonitorsPageHeader = () => ( - -
- -
-
); diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index 370dd2e802bdfc..0900b6f1685dcd 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -213,11 +213,7 @@ export class UptimePlugin id: 'synthetics', euiIconType: 'logoObservability', order: 8400, - title: - PLUGIN.SYNTHETICS + - i18n.translate('xpack.synthetics.overview.headingBeta', { - defaultMessage: ' (beta)', - }), + title: PLUGIN.SYNTHETICS, category: DEFAULT_APP_CATEGORIES.observability, keywords: appKeywords, deepLinks: [], @@ -313,7 +309,6 @@ function registerUptimeRoutesWithNavigation( path: OVERVIEW_ROUTE, matchFullPath: false, ignoreTrailingSlash: true, - isBetaFeature: true, }, ], }, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 290368a66b4ed9..9d7fb0e13680d7 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -34462,8 +34462,6 @@ "xpack.synthetics.coreVitals.fcpTooltip": "First Contentful Paint (FCP) se concentre sur le rendu initial et mesure la durée entre le début du chargement d'une page et le moment où une partie du contenu de la page s'affiche à l'écran.", "xpack.synthetics.coreVitals.lcp.help": "Largest Contentful Paint mesure les performances de chargement. Pour offrir une expérience agréable aux utilisateurs, le LCP doit survenir dans les 2,5 secondes du début de chargement de la page.", "xpack.synthetics.createMonitor.pageHeader.title": "Créer le moniteur", - "xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.experimentalLabel": "Préversion technique", - "xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.experimentalTooltip": "Prévisualisez la méthode la plus rapide permettant de créer des scripts de monitoring Elastic Synthetics avec notre enregistreur Elastic Synthetics", "xpack.synthetics.createPackagePolicy.stepConfigure.headerField.addHeader.label": "Ajouter un en-tête", "xpack.synthetics.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel": "Facultatif", "xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.closeButtonLabel": "Fermer le menu volant du script", @@ -35043,7 +35041,6 @@ "xpack.synthetics.monitorManagement.websiteUrlLabel": "URL de site web", "xpack.synthetics.monitorManagement.websiteUrlPlaceholder": "Entrer l'URL d'un site web", "xpack.synthetics.monitorOverviewTab.title": "Aperçu", - "xpack.synthetics.monitors.management.betaLabel": "Cette fonctionnalité est en version bêta et susceptible d'être modifiée. La conception et le code sont moins matures que les fonctionnalités officielles en disponibilité générale et sont fournis tels quels sans aucune garantie. Les fonctionnalités en version bêta ne sont pas soumises à l'accord de niveau de service des fonctionnalités officielles en disponibilité générale.", "xpack.synthetics.monitors.pageHeader.createButton.label": "Créer le moniteur", "xpack.synthetics.monitors.pageHeader.title": "Moniteurs", "xpack.synthetics.monitorsPage.errors": "Erreurs", @@ -35129,7 +35126,6 @@ "xpack.synthetics.overview.groupPopover.project.label": "Projet", "xpack.synthetics.overview.groupPopover.tag.label": "Balise", "xpack.synthetics.overview.heading": "Moniteurs", - "xpack.synthetics.overview.headingBeta": " (bêta)", "xpack.synthetics.overview.headingBetaSection": "Synthetics", "xpack.synthetics.overview.monitors.label": "Moniteurs", "xpack.synthetics.overview.noMonitorsFoundContent": "Essayez d'affiner votre recherche.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c1c8d3f14fe4e9..583a0f2180f70b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34441,8 +34441,6 @@ "xpack.synthetics.coreVitals.fcpTooltip": "初回コンテンツの描画(FCP)は初期のレンダリングに集中し、ページの読み込みが開始してから、ページのコンテンツのいずれかの部分が画面に表示されるときまでの時間を測定します。", "xpack.synthetics.coreVitals.lcp.help": "最大コンテンツの描画(LCP)は読み込みパフォーマンスを計測します。優れたユーザーエクスペリエンスを実現するには、ページの読み込みが開始した後、2.5秒以内にLCPが実行されるようにしてください。", "xpack.synthetics.createMonitor.pageHeader.title": "監視の作成", - "xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.experimentalLabel": "テクニカルプレビュー", - "xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.experimentalTooltip": "Elastic Synthetics Recorderを使用してElastic Synthetics監視スクリプトを作成する最も簡単な方法をプレビュー", "xpack.synthetics.createPackagePolicy.stepConfigure.headerField.addHeader.label": "ヘッダーを追加", "xpack.synthetics.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel": "オプション", "xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.closeButtonLabel": "スクリプトフライアウトを閉じる", @@ -35022,7 +35020,6 @@ "xpack.synthetics.monitorManagement.websiteUrlLabel": "WebサイトのURL", "xpack.synthetics.monitorManagement.websiteUrlPlaceholder": "WebサイトURLを入力", "xpack.synthetics.monitorOverviewTab.title": "概要", - "xpack.synthetics.monitors.management.betaLabel": "この機能はベータ段階で、変更される可能性があります。デザインとコードは正式に一般公開された機能より完成度が低く、現状のまま保証なしで提供されています。ベータ機能は、正式に一般公開された機能に適用されるサポートサービスレベル契約の対象外です。", "xpack.synthetics.monitors.pageHeader.createButton.label": "監視の作成", "xpack.synthetics.monitors.pageHeader.title": "監視", "xpack.synthetics.monitorsPage.errors": "エラー", @@ -35108,7 +35105,6 @@ "xpack.synthetics.overview.groupPopover.project.label": "プロジェクト", "xpack.synthetics.overview.groupPopover.tag.label": "タグ", "xpack.synthetics.overview.heading": "監視", - "xpack.synthetics.overview.headingBeta": " (ベータ)", "xpack.synthetics.overview.headingBetaSection": "Synthetics", "xpack.synthetics.overview.monitors.label": "監視", "xpack.synthetics.overview.noMonitorsFoundContent": "検索を更新してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 40989bc638cc50..704e318ed4aee3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -34457,8 +34457,6 @@ "xpack.synthetics.coreVitals.fcpTooltip": "首次内容绘制 (FCP) 侧重于初始渲染,并测量从页面开始加载到页面内容的任何部分显示在屏幕的时间。", "xpack.synthetics.coreVitals.lcp.help": "最大内容绘制用于衡量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的 2.5 秒内执行。", "xpack.synthetics.createMonitor.pageHeader.title": "创建监测", - "xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.experimentalLabel": "技术预览", - "xpack.synthetics.createPackagePolicy.stepConfigure.browser.scriptRecorder.experimentalTooltip": "预览通过 Elastic Synthetics 记录器创建 Elastic Synthetics 监测脚本的最快方式", "xpack.synthetics.createPackagePolicy.stepConfigure.headerField.addHeader.label": "添加标头", "xpack.synthetics.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel": "可选", "xpack.synthetics.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.closeButtonLabel": "关闭脚本浮出控件", @@ -35038,7 +35036,6 @@ "xpack.synthetics.monitorManagement.websiteUrlLabel": "网站 URL", "xpack.synthetics.monitorManagement.websiteUrlPlaceholder": "输入网站 URL", "xpack.synthetics.monitorOverviewTab.title": "概览", - "xpack.synthetics.monitors.management.betaLabel": "此功能为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能的支持服务水平协议约束。", "xpack.synthetics.monitors.pageHeader.createButton.label": "创建监测", "xpack.synthetics.monitors.pageHeader.title": "监测", "xpack.synthetics.monitorsPage.errors": "错误", @@ -35124,7 +35121,6 @@ "xpack.synthetics.overview.groupPopover.project.label": "项目", "xpack.synthetics.overview.groupPopover.tag.label": "标签", "xpack.synthetics.overview.heading": "监测", - "xpack.synthetics.overview.headingBeta": " (公测版)", "xpack.synthetics.overview.headingBetaSection": "Synthetics", "xpack.synthetics.overview.monitors.label": "监测", "xpack.synthetics.overview.noMonitorsFoundContent": "请尝试优化您的搜索。", From 6ac4e1919c25532c6f22ac24d62efc1d4427e6f6 Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Mon, 24 Apr 2023 11:54:43 +0200 Subject: [PATCH 34/65] [Infrastructure UI] Implement inventory views CRUD endpoints (#154900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Part of #152617 Closes #155158 This PR implements the CRUD endpoints for the inventory views. Following the approach used for the LogViews service, it exposes a client that abstracts all the logic concerned to the `inventory-view` saved objects. It also follows the guideline provided for [Versioning interfaces](https://docs.elastic.dev/kibana-dev-docs/versioning-interfaces) and [Versioning HTTP APIs](https://docs.elastic.dev/kibana-dev-docs/versioning-http-apis), preparing for the serverless. ## 🤓 Tips for the reviewer You can open the Kibana dev tools and play with the following snippet to test the create APIs, or you can perform the same requests with your preferred client: ``` // Get all GET kbn:/api/infra/inventory_views // Create one POST kbn:/api/infra/inventory_views { "attributes": { "name": "My inventory view" } } // Get one GET kbn:/api/infra/inventory_views/ // Update one PUT kbn:/api/infra/inventory_views/ { "attributes": { "name": "My inventory view 2" } } // Delete one DELETE kbn:/api/infra/inventory_views/ ``` ## 👣 Next steps - Replicate the same logic for the metrics explorer saved object - Create a client-side abstraction to consume the service - Update the existing react custom hooks to consume the endpoint --------- Co-authored-by: Marco Antonio Ghiani Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-io-ts-utils/index.ts | 2 +- x-pack/plugins/infra/common/http_api/index.ts | 6 + .../http_api/inventory_views/v1/common.ts | 66 ++++ .../v1/create_inventory_view.ts | 29 ++ .../inventory_views/v1/find_inventory_view.ts | 34 ++ .../inventory_views/v1/get_inventory_view.ts | 14 + .../http_api/inventory_views/v1/index.ts | 12 + .../v1/update_inventory_view.ts | 29 ++ .../plugins/infra/common/http_api/latest.ts | 8 + .../infra/common/inventory_views/defaults.ts | 52 +++ .../infra/common/inventory_views/index.ts | 9 + .../inventory_views/inventory_view.mock.ts | 24 ++ .../infra/common/inventory_views/types.ts | 35 ++ .../common/metrics_explorer_views/defaults.ts | 51 +++ .../common/metrics_explorer_views/index.ts | 8 + .../metric_explorer_view.mock.ts | 24 ++ .../common/metrics_explorer_views/types.ts | 35 ++ x-pack/plugins/infra/server/infra_server.ts | 2 + x-pack/plugins/infra/server/mocks.ts | 2 + x-pack/plugins/infra/server/plugin.ts | 19 +- .../server/routes/inventory_views/README.md | 350 ++++++++++++++++++ .../inventory_views/create_inventory_view.ts | 58 +++ .../inventory_views/delete_inventory_view.ts | 54 +++ .../inventory_views/find_inventory_view.ts | 49 +++ .../inventory_views/get_inventory_view.ts | 59 +++ .../server/routes/inventory_views/index.ts | 23 ++ .../inventory_views/update_inventory_view.ts | 65 ++++ .../infra/server/saved_objects/index.ts | 2 + .../saved_objects/inventory_view/index.ts | 12 + .../inventory_view_saved_object.ts | 39 ++ .../saved_objects/inventory_view/types.ts | 27 ++ .../metrics_explorer_view/index.ts | 12 + .../metrics_explorer_view_saved_object.ts | 39 ++ .../metrics_explorer_view/types.ts | 21 ++ .../server/services/inventory_views/index.ts | 14 + .../inventory_views_client.mock.ts | 16 + .../inventory_views_client.test.ts | 255 +++++++++++++ .../inventory_views/inventory_views_client.ts | 199 ++++++++++ .../inventory_views_service.mock.ts | 18 + .../inventory_views_service.ts | 39 ++ .../server/services/inventory_views/types.ts | 45 +++ x-pack/plugins/infra/server/types.ts | 2 + 42 files changed, 1855 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts create mode 100644 x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts create mode 100644 x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts create mode 100644 x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts create mode 100644 x-pack/plugins/infra/common/http_api/inventory_views/v1/index.ts create mode 100644 x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts create mode 100644 x-pack/plugins/infra/common/http_api/latest.ts create mode 100644 x-pack/plugins/infra/common/inventory_views/defaults.ts create mode 100644 x-pack/plugins/infra/common/inventory_views/index.ts create mode 100644 x-pack/plugins/infra/common/inventory_views/inventory_view.mock.ts create mode 100644 x-pack/plugins/infra/common/inventory_views/types.ts create mode 100644 x-pack/plugins/infra/common/metrics_explorer_views/defaults.ts create mode 100644 x-pack/plugins/infra/common/metrics_explorer_views/index.ts create mode 100644 x-pack/plugins/infra/common/metrics_explorer_views/metric_explorer_view.mock.ts create mode 100644 x-pack/plugins/infra/common/metrics_explorer_views/types.ts create mode 100644 x-pack/plugins/infra/server/routes/inventory_views/README.md create mode 100644 x-pack/plugins/infra/server/routes/inventory_views/create_inventory_view.ts create mode 100644 x-pack/plugins/infra/server/routes/inventory_views/delete_inventory_view.ts create mode 100644 x-pack/plugins/infra/server/routes/inventory_views/find_inventory_view.ts create mode 100644 x-pack/plugins/infra/server/routes/inventory_views/get_inventory_view.ts create mode 100644 x-pack/plugins/infra/server/routes/inventory_views/index.ts create mode 100644 x-pack/plugins/infra/server/routes/inventory_views/update_inventory_view.ts create mode 100644 x-pack/plugins/infra/server/saved_objects/inventory_view/index.ts create mode 100644 x-pack/plugins/infra/server/saved_objects/inventory_view/inventory_view_saved_object.ts create mode 100644 x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts create mode 100644 x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/index.ts create mode 100644 x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/metrics_explorer_view_saved_object.ts create mode 100644 x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/types.ts create mode 100644 x-pack/plugins/infra/server/services/inventory_views/index.ts create mode 100644 x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts create mode 100644 x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.test.ts create mode 100644 x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts create mode 100644 x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.mock.ts create mode 100644 x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.ts create mode 100644 x-pack/plugins/infra/server/services/inventory_views/types.ts diff --git a/packages/kbn-io-ts-utils/index.ts b/packages/kbn-io-ts-utils/index.ts index e52e4d429829e3..e8e6da2e7b59e9 100644 --- a/packages/kbn-io-ts-utils/index.ts +++ b/packages/kbn-io-ts-utils/index.ts @@ -7,7 +7,7 @@ */ export type { IndexPatternType } from './src/index_pattern_rt'; -export type { NonEmptyStringBrand } from './src/non_empty_string_rt'; +export type { NonEmptyString, NonEmptyStringBrand } from './src/non_empty_string_rt'; export { deepExactRt } from './src/deep_exact_rt'; export { indexPatternRt } from './src/index_pattern_rt'; diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts index 934e4200cb31ce..355c5925702f7e 100644 --- a/x-pack/plugins/infra/common/http_api/index.ts +++ b/x-pack/plugins/infra/common/http_api/index.ts @@ -14,3 +14,9 @@ export * from './log_alerts'; export * from './snapshot_api'; export * from './host_details'; export * from './infra'; + +/** + * Exporting versioned APIs types + */ +export * from './latest'; +export * as inventoryViewsV1 from './inventory_views/v1'; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts new file mode 100644 index 00000000000000..c229170b8007b5 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; +import { either } from 'fp-ts/Either'; + +export const INVENTORY_VIEW_URL = '/api/infra/inventory_views'; +export const INVENTORY_VIEW_URL_ENTITY = `${INVENTORY_VIEW_URL}/{inventoryViewId}`; +export const getInventoryViewUrl = (inventoryViewId?: string) => + [INVENTORY_VIEW_URL, inventoryViewId].filter(Boolean).join('/'); + +const inventoryViewIdRT = new rt.Type( + 'InventoryViewId', + rt.string.is, + (u, c) => + either.chain(rt.string.validate(u, c), (id) => { + return id === '0' + ? rt.failure(u, c, `The inventory view with id ${id} is not configurable.`) + : rt.success(id); + }), + String +); + +export const inventoryViewRequestParamsRT = rt.type({ + inventoryViewId: inventoryViewIdRT, +}); + +export type InventoryViewRequestParams = rt.TypeOf; + +export const inventoryViewRequestQueryRT = rt.partial({ + sourceId: rt.string, +}); + +export type InventoryViewRequestQuery = rt.TypeOf; + +const inventoryViewAttributesResponseRT = rt.intersection([ + rt.strict({ + name: nonEmptyStringRt, + isDefault: rt.boolean, + isStatic: rt.boolean, + }), + rt.UnknownRecord, +]); + +const inventoryViewResponseRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + attributes: inventoryViewAttributesResponseRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export const inventoryViewResponsePayloadRT = rt.type({ + data: inventoryViewResponseRT, +}); + +export type GetInventoryViewResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts new file mode 100644 index 00000000000000..99350daa358b07 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const createInventoryViewAttributesRequestPayloadRT = rt.intersection([ + rt.type({ + name: nonEmptyStringRt, + }), + rt.UnknownRecord, + rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })), +]); + +export type CreateInventoryViewAttributesRequestPayload = rt.TypeOf< + typeof createInventoryViewAttributesRequestPayloadRT +>; + +export const createInventoryViewRequestPayloadRT = rt.type({ + attributes: createInventoryViewAttributesRequestPayloadRT, +}); + +export type CreateInventoryViewRequestPayload = rt.TypeOf< + typeof createInventoryViewRequestPayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts new file mode 100644 index 00000000000000..24812ccb435851 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const findInventoryViewAttributesResponseRT = rt.strict({ + name: nonEmptyStringRt, + isDefault: rt.boolean, + isStatic: rt.boolean, +}); + +const findInventoryViewResponseRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + attributes: findInventoryViewAttributesResponseRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export const findInventoryViewResponsePayloadRT = rt.type({ + data: rt.array(findInventoryViewResponseRT), +}); + +export type FindInventoryViewResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts new file mode 100644 index 00000000000000..3e862bdaa3388c --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const getInventoryViewRequestParamsRT = rt.type({ + inventoryViewId: rt.string, +}); + +export type GetInventoryViewRequestParams = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/index.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/index.ts new file mode 100644 index 00000000000000..74f0d3b6962a1f --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './common'; +export * from './get_inventory_view'; +export * from './find_inventory_view'; +export * from './create_inventory_view'; +export * from './update_inventory_view'; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts new file mode 100644 index 00000000000000..7a2d33ebd61387 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const updateInventoryViewAttributesRequestPayloadRT = rt.intersection([ + rt.type({ + name: nonEmptyStringRt, + }), + rt.UnknownRecord, + rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })), +]); + +export type UpdateInventoryViewAttributesRequestPayload = rt.TypeOf< + typeof updateInventoryViewAttributesRequestPayloadRT +>; + +export const updateInventoryViewRequestPayloadRT = rt.type({ + attributes: updateInventoryViewAttributesRequestPayloadRT, +}); + +export type UpdateInventoryViewRequestPayload = rt.TypeOf< + typeof updateInventoryViewRequestPayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/latest.ts b/x-pack/plugins/infra/common/http_api/latest.ts new file mode 100644 index 00000000000000..519da4a60dec1b --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/latest.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './inventory_views/v1'; diff --git a/x-pack/plugins/infra/common/inventory_views/defaults.ts b/x-pack/plugins/infra/common/inventory_views/defaults.ts new file mode 100644 index 00000000000000..ae78000968995e --- /dev/null +++ b/x-pack/plugins/infra/common/inventory_views/defaults.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { NonEmptyString } from '@kbn/io-ts-utils'; +import type { InventoryViewAttributes } from './types'; + +export const staticInventoryViewId = '0'; + +export const staticInventoryViewAttributes: InventoryViewAttributes = { + name: i18n.translate('xpack.infra.savedView.defaultViewNameHosts', { + defaultMessage: 'Default view', + }) as NonEmptyString, + isDefault: false, + isStatic: true, + metric: { + type: 'cpu', + }, + groupBy: [], + nodeType: 'host', + view: 'map', + customOptions: [], + boundsOverride: { + max: 1, + min: 0, + }, + autoBounds: true, + accountId: '', + region: '', + customMetrics: [], + legend: { + palette: 'cool', + steps: 10, + reverseColors: false, + }, + source: 'default', + sort: { + by: 'name', + direction: 'desc', + }, + timelineOpen: false, + filterQuery: { + kind: 'kuery', + expression: '', + }, + time: Date.now(), + autoReload: false, +}; diff --git a/x-pack/plugins/infra/common/inventory_views/index.ts b/x-pack/plugins/infra/common/inventory_views/index.ts new file mode 100644 index 00000000000000..ae809a6c7c6157 --- /dev/null +++ b/x-pack/plugins/infra/common/inventory_views/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './defaults'; +export * from './types'; diff --git a/x-pack/plugins/infra/common/inventory_views/inventory_view.mock.ts b/x-pack/plugins/infra/common/inventory_views/inventory_view.mock.ts new file mode 100644 index 00000000000000..a8f5ef6ce181b9 --- /dev/null +++ b/x-pack/plugins/infra/common/inventory_views/inventory_view.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { staticInventoryViewAttributes } from './defaults'; +import type { InventoryView, InventoryViewAttributes } from './types'; + +export const createInventoryViewMock = ( + id: string, + attributes: InventoryViewAttributes, + updatedAt?: number, + version?: string +): InventoryView => ({ + id, + attributes: { + ...staticInventoryViewAttributes, + ...attributes, + }, + updatedAt, + version, +}); diff --git a/x-pack/plugins/infra/common/inventory_views/types.ts b/x-pack/plugins/infra/common/inventory_views/types.ts new file mode 100644 index 00000000000000..49979c1063efa3 --- /dev/null +++ b/x-pack/plugins/infra/common/inventory_views/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const inventoryViewAttributesRT = rt.intersection([ + rt.strict({ + name: nonEmptyStringRt, + isDefault: rt.boolean, + isStatic: rt.boolean, + }), + rt.UnknownRecord, +]); + +export type InventoryViewAttributes = rt.TypeOf; + +export const inventoryViewRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + attributes: inventoryViewAttributesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export type InventoryView = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/defaults.ts b/x-pack/plugins/infra/common/metrics_explorer_views/defaults.ts new file mode 100644 index 00000000000000..88771d1a76fcbc --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_explorer_views/defaults.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { NonEmptyString } from '@kbn/io-ts-utils'; +import type { MetricsExplorerViewAttributes } from './types'; + +export const staticMetricsExplorerViewId = 'static'; + +export const staticMetricsExplorerViewAttributes: MetricsExplorerViewAttributes = { + name: i18n.translate('xpack.infra.savedView.defaultViewNameHosts', { + defaultMessage: 'Default view', + }) as NonEmptyString, + isDefault: false, + isStatic: true, + options: { + aggregation: 'avg', + metrics: [ + { + aggregation: 'avg', + field: 'system.cpu.total.norm.pct', + color: 'color0', + }, + { + aggregation: 'avg', + field: 'kubernetes.pod.cpu.usage.node.pct', + color: 'color1', + }, + { + aggregation: 'avg', + field: 'docker.cpu.total.pct', + color: 'color2', + }, + ], + source: 'default', + }, + chartOptions: { + type: 'line', + yAxisMode: 'fromZero', + stack: false, + }, + currentTimerange: { + from: 'now-1h', + to: 'now', + interval: '>=10s', + }, +}; diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/index.ts b/x-pack/plugins/infra/common/metrics_explorer_views/index.ts new file mode 100644 index 00000000000000..6cc0ccaa93a6d1 --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_explorer_views/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './types'; diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/metric_explorer_view.mock.ts b/x-pack/plugins/infra/common/metrics_explorer_views/metric_explorer_view.mock.ts new file mode 100644 index 00000000000000..e921c37dd21f8b --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_explorer_views/metric_explorer_view.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { staticMetricsExplorerViewAttributes } from './defaults'; +import type { MetricsExplorerView, MetricsExplorerViewAttributes } from './types'; + +export const createmetricsExplorerViewMock = ( + id: string, + attributes: MetricsExplorerViewAttributes, + updatedAt?: number, + version?: string +): MetricsExplorerView => ({ + id, + attributes: { + ...staticMetricsExplorerViewAttributes, + ...attributes, + }, + updatedAt, + version, +}); diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/types.ts b/x-pack/plugins/infra/common/metrics_explorer_views/types.ts new file mode 100644 index 00000000000000..47ecb06ceace56 --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_explorer_views/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const metricsExplorerViewAttributesRT = rt.intersection([ + rt.strict({ + name: nonEmptyStringRt, + isDefault: rt.boolean, + isStatic: rt.boolean, + }), + rt.UnknownRecord, +]); + +export type MetricsExplorerViewAttributes = rt.TypeOf; + +export const metricsExplorerViewRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + attributes: metricsExplorerViewAttributesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export type MetricsExplorerView = rt.TypeOf; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index bdd4f7d841217c..3b6ea0333f236e 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -8,6 +8,7 @@ import { InfraBackendLibs } from './lib/infra_types'; import { initGetHostsAnomaliesRoute, initGetK8sAnomaliesRoute } from './routes/infra_ml'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; +import { initInventoryViewRoutes } from './routes/inventory_views'; import { initIpToHostName } from './routes/ip_to_hostname'; import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; import { @@ -61,6 +62,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initMetricsAPIRoute(libs); initMetadataRoute(libs); initInventoryMetaRoute(libs); + initInventoryViewRoutes(libs); initGetLogAlertsChartPreviewDataRoute(libs); initProcessListRoute(libs); initOverviewRoute(libs); diff --git a/x-pack/plugins/infra/server/mocks.ts b/x-pack/plugins/infra/server/mocks.ts index 5b587a1fe80d50..5a97f4a7d9a524 100644 --- a/x-pack/plugins/infra/server/mocks.ts +++ b/x-pack/plugins/infra/server/mocks.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { createInventoryViewsServiceStartMock } from './services/inventory_views/inventory_views_service.mock'; import { createLogViewsServiceSetupMock, createLogViewsServiceStartMock, @@ -23,6 +24,7 @@ const createInfraSetupMock = () => { const createInfraStartMock = () => { const infraStartMock: jest.Mocked = { getMetricIndices: jest.fn(), + inventoryViews: createInventoryViewsServiceStartMock(), logViews: createLogViewsServiceStartMock(), }; return infraStartMock; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 7e7507c1d2e45c..2c114bb75d6e55 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -20,8 +20,6 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { defaultLogViewsStaticConfig } from '../common/log_views'; import { publicConfigKeys } from '../common/plugin_config_types'; -import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; -import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; import { configDeprecations, getInfraDeprecationsFactory } from './deprecations'; import { LOGS_FEATURE, METRICS_FEATURE } from './features'; import { initInfraServer } from './infra_server'; @@ -43,7 +41,12 @@ import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; import { makeGetMetricIndices } from './lib/metrics/make_get_metric_indices'; import { infraSourceConfigurationSavedObjectType, InfraSources } from './lib/sources'; import { InfraSourceStatus } from './lib/source_status'; -import { logViewSavedObjectType } from './saved_objects'; +import { + inventoryViewSavedObjectType, + logViewSavedObjectType, + metricsExplorerViewSavedObjectType, +} from './saved_objects'; +import { InventoryViewsService } from './services/inventory_views'; import { LogEntriesService } from './services/log_entries'; import { LogViewsService } from './services/log_views'; import { RulesService } from './services/rules'; @@ -117,6 +120,7 @@ export class InfraServerPlugin private logsRules: RulesService; private metricsRules: RulesService; + private inventoryViews: InventoryViewsService; private logViews: LogViewsService; constructor(context: PluginInitializerContext) { @@ -134,6 +138,7 @@ export class InfraServerPlugin this.logger.get('metricsRules') ); + this.inventoryViews = new InventoryViewsService(this.logger.get('inventoryViews')); this.logViews = new LogViewsService(this.logger.get('logViews')); } @@ -148,6 +153,7 @@ export class InfraServerPlugin sources, } ); + const inventoryViews = this.inventoryViews.setup(); const logViews = this.logViews.setup(); // register saved object types @@ -229,11 +235,17 @@ export class InfraServerPlugin return { defineInternalSourceConfiguration: sources.defineInternalSourceConfiguration.bind(sources), + inventoryViews, logViews, } as InfraPluginSetup; } start(core: CoreStart, plugins: InfraServerPluginStartDeps) { + const inventoryViews = this.inventoryViews.start({ + infraSources: this.libs.sources, + savedObjects: core.savedObjects, + }); + const logViews = this.logViews.start({ infraSources: this.libs.sources, savedObjects: core.savedObjects, @@ -247,6 +259,7 @@ export class InfraServerPlugin }); return { + inventoryViews, logViews, getMetricIndices: makeGetMetricIndices(this.libs.sources), }; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/README.md b/x-pack/plugins/infra/server/routes/inventory_views/README.md new file mode 100644 index 00000000000000..8a09aedef1b75a --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/README.md @@ -0,0 +1,350 @@ +# Inventory Views CRUD api + +## Find all: `GET /api/infra/inventory_views` + +Retrieves all inventory views in a reduced version. + +### Request + +- **Method**: GET +- **Path**: /api/infra/inventory_views +- **Query params**: + - `sourceId` _(optional)_: Specify a source id related to the inventory views. Default value: `default`. + +### Response + +```json +GET /api/infra/inventory_views + +Status code: 200 + +{ + "data": [ + { + "id": "static", + "attributes": { + "name": "Default view", + "isDefault": false, + "isStatic": true + } + }, + { + "id": "927ad6a0-da0c-11ed-9487-41e9b90f96b9", + "version": "WzQwMiwxXQ==", + "updatedAt": 1681398305034, + "attributes": { + "name": "Ad-hoc", + "isDefault": true, + "isStatic": false + } + }, + { + "id": "c301ef20-da0c-11ed-aac0-77131228e6f1", + "version": "WzQxMCwxXQ==", + "updatedAt": 1681398386450, + "attributes": { + "name": "Custom", + "isDefault": false, + "isStatic": false + } + } + ] +} +``` + +## Get one: `GET /api/infra/inventory_views/{inventoryViewId}` + +Retrieves a single inventory view by ID + +### Request + +- **Method**: GET +- **Path**: /api/infra/inventory_views/{inventoryViewId} +- **Query params**: + - `sourceId` _(optional)_: Specify a source id related to the inventory view. Default value: `default`. + +### Response + +```json +GET /api/infra/inventory_views/927ad6a0-da0c-11ed-9487-41e9b90f96b9 + +Status code: 200 + +{ + "data": { + "id": "927ad6a0-da0c-11ed-9487-41e9b90f96b9", + "version": "WzQwMiwxXQ==", + "updatedAt": 1681398305034, + "attributes": { + "name": "Ad-hoc", + "isDefault": true, + "isStatic": false, + "metric": { + "type": "cpu" + }, + "sort": { + "by": "name", + "direction": "desc" + }, + "groupBy": [], + "nodeType": "host", + "view": "map", + "customOptions": [], + "customMetrics": [], + "boundsOverride": { + "max": 1, + "min": 0 + }, + "autoBounds": true, + "accountId": "", + "region": "", + "autoReload": false, + "filterQuery": { + "expression": "", + "kind": "kuery" + }, + "legend": { + "palette": "cool", + "reverseColors": false, + "steps": 10 + }, + "timelineOpen": false + } + } +} +``` + +```json +GET /api/infra/inventory_views/random-id + +Status code: 404 + +{ + "statusCode": 404, + "error": "Not Found", + "message": "Saved object [inventory-view/random-id] not found" +} +``` + +## Create one: `POST /api/infra/inventory_views` + +Creates a new inventory view. + +### Request + +- **Method**: POST +- **Path**: /api/infra/inventory_views +- **Request body**: + ```json + { + "attributes": { + "name": "View name", + "metric": { + "type": "cpu" + }, + "sort": { + "by": "name", + "direction": "desc" + }, + //... + } + } + ``` + +### Response + +```json +POST /api/infra/inventory_views + +Status code: 201 + +{ + "data": { + "id": "927ad6a0-da0c-11ed-9487-41e9b90f96b9", + "version": "WzQwMiwxXQ==", + "updatedAt": 1681398305034, + "attributes": { + "name": "View name", + "isDefault": false, + "isStatic": false, + "metric": { + "type": "cpu" + }, + "sort": { + "by": "name", + "direction": "desc" + }, + "groupBy": [], + "nodeType": "host", + "view": "map", + "customOptions": [], + "customMetrics": [], + "boundsOverride": { + "max": 1, + "min": 0 + }, + "autoBounds": true, + "accountId": "", + "region": "", + "autoReload": false, + "filterQuery": { + "expression": "", + "kind": "kuery" + }, + "legend": { + "palette": "cool", + "reverseColors": false, + "steps": 10 + }, + "timelineOpen": false + } + } +} +``` + +Send in the payload a `name` attribute already held by another view: +```json +POST /api/infra/inventory_views + +Status code: 409 + +{ + "statusCode": 409, + "error": "Conflict", + "message": "A view with that name already exists." +} +``` + +## Update one: `PUT /api/infra/inventory_views/{inventoryViewId}` + +Updates an inventory view. + +Any attribute can be updated except for `isDefault` and `isStatic`, which are derived by the source configuration preference set by the user. + +### Request + +- **Method**: PUT +- **Path**: /api/infra/inventory_views/{inventoryViewId} +- **Query params**: + - `sourceId` _(optional)_: Specify a source id related to the inventory view. Default value: `default`. +- **Request body**: + ```json + { + "attributes": { + "name": "View name", + "metric": { + "type": "cpu" + }, + "sort": { + "by": "name", + "direction": "desc" + }, + //... + } + } + ``` + +### Response + +```json +PUT /api/infra/inventory_views/927ad6a0-da0c-11ed-9487-41e9b90f96b9 + +Status code: 200 + +{ + "data": { + "id": "927ad6a0-da0c-11ed-9487-41e9b90f96b9", + "version": "WzQwMiwxXQ==", + "updatedAt": 1681398305034, + "attributes": { + "name": "View name", + "isDefault": false, + "isStatic": false, + "metric": { + "type": "cpu" + }, + "sort": { + "by": "name", + "direction": "desc" + }, + "groupBy": [], + "nodeType": "host", + "view": "map", + "customOptions": [], + "customMetrics": [], + "boundsOverride": { + "max": 1, + "min": 0 + }, + "autoBounds": true, + "accountId": "", + "region": "", + "autoReload": false, + "filterQuery": { + "expression": "", + "kind": "kuery" + }, + "legend": { + "palette": "cool", + "reverseColors": false, + "steps": 10 + }, + "timelineOpen": false + } + } +} +``` + +```json +PUT /api/infra/inventory_views/random-id + +Status code: 404 + +{ + "statusCode": 404, + "error": "Not Found", + "message": "Saved object [inventory-view/random-id] not found" +} +``` + +Send in the payload a `name` attribute already held by another view: +```json +PUT /api/infra/inventory_views/927ad6a0-da0c-11ed-9487-41e9b90f96b9 + +Status code: 409 + +{ + "statusCode": 409, + "error": "Conflict", + "message": "A view with that name already exists." +} +``` + +## Delete one: `DELETE /api/infra/inventory_views/{inventoryViewId}` + +Deletes an inventory view. + +### Request + +- **Method**: DELETE +- **Path**: /api/infra/inventory_views/{inventoryViewId} + +### Response + +```json +DELETE /api/infra/inventory_views/927ad6a0-da0c-11ed-9487-41e9b90f96b9 + +Status code: 204 No content +``` + +```json +DELETE /api/infra/inventory_views/random-id + +Status code: 404 + +{ + "statusCode": 404, + "error": "Not Found", + "message": "Saved object [inventory-view/random-id] not found" +} +``` diff --git a/x-pack/plugins/infra/server/routes/inventory_views/create_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/create_inventory_view.ts new file mode 100644 index 00000000000000..8f3d52db7a6dd4 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/create_inventory_view.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isBoom } from '@hapi/boom'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { + createInventoryViewRequestPayloadRT, + inventoryViewResponsePayloadRT, + INVENTORY_VIEW_URL, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initCreateInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'post', + path: INVENTORY_VIEW_URL, + validate: { + body: createValidationFunction(createInventoryViewRequestPayloadRT), + }, + }, + async (_requestContext, request, response) => { + const { body } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + const inventoryView = await inventoryViewsClient.create(body.attributes); + + return response.custom({ + statusCode: 201, + body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }), + }); + } catch (error) { + if (isBoom(error)) { + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/delete_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/delete_inventory_view.ts new file mode 100644 index 00000000000000..83ad61fc46c528 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/delete_inventory_view.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isBoom } from '@hapi/boom'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { + inventoryViewRequestParamsRT, + INVENTORY_VIEW_URL_ENTITY, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initDeleteInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'delete', + path: INVENTORY_VIEW_URL_ENTITY, + validate: { + params: createValidationFunction(inventoryViewRequestParamsRT), + }, + }, + async (_requestContext, request, response) => { + const { params } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + await inventoryViewsClient.delete(params.inventoryViewId); + + return response.noContent(); + } catch (error) { + if (isBoom(error)) { + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/find_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/find_inventory_view.ts new file mode 100644 index 00000000000000..abdfc2f8749e4c --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/find_inventory_view.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createValidationFunction } from '../../../common/runtime_types'; +import { + findInventoryViewResponsePayloadRT, + inventoryViewRequestQueryRT, + INVENTORY_VIEW_URL, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initFindInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'get', + path: INVENTORY_VIEW_URL, + validate: { + query: createValidationFunction(inventoryViewRequestQueryRT), + }, + }, + async (_requestContext, request, response) => { + const { query } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + const inventoryViewsList = await inventoryViewsClient.find(query); + + return response.ok({ + body: findInventoryViewResponsePayloadRT.encode({ data: inventoryViewsList }), + }); + } catch (error) { + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/get_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/get_inventory_view.ts new file mode 100644 index 00000000000000..1a5f5adec136dd --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/get_inventory_view.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isBoom } from '@hapi/boom'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { + inventoryViewResponsePayloadRT, + inventoryViewRequestQueryRT, + INVENTORY_VIEW_URL_ENTITY, + getInventoryViewRequestParamsRT, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initGetInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'get', + path: INVENTORY_VIEW_URL_ENTITY, + validate: { + params: createValidationFunction(getInventoryViewRequestParamsRT), + query: createValidationFunction(inventoryViewRequestQueryRT), + }, + }, + async (_requestContext, request, response) => { + const { params, query } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + const inventoryView = await inventoryViewsClient.get(params.inventoryViewId, query); + + return response.ok({ + body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }), + }); + } catch (error) { + if (isBoom(error)) { + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/index.ts b/x-pack/plugins/infra/server/routes/inventory_views/index.ts new file mode 100644 index 00000000000000..55cee58a8a4644 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { InfraBackendLibs } from '../../lib/infra_types'; +import { initCreateInventoryViewRoute } from './create_inventory_view'; +import { initDeleteInventoryViewRoute } from './delete_inventory_view'; +import { initFindInventoryViewRoute } from './find_inventory_view'; +import { initGetInventoryViewRoute } from './get_inventory_view'; +import { initUpdateInventoryViewRoute } from './update_inventory_view'; + +export const initInventoryViewRoutes = ( + dependencies: Pick +) => { + initCreateInventoryViewRoute(dependencies); + initDeleteInventoryViewRoute(dependencies); + initFindInventoryViewRoute(dependencies); + initGetInventoryViewRoute(dependencies); + initUpdateInventoryViewRoute(dependencies); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/update_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/update_inventory_view.ts new file mode 100644 index 00000000000000..d2b583437d177b --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/update_inventory_view.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isBoom } from '@hapi/boom'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { + inventoryViewRequestParamsRT, + inventoryViewRequestQueryRT, + inventoryViewResponsePayloadRT, + INVENTORY_VIEW_URL_ENTITY, + updateInventoryViewRequestPayloadRT, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initUpdateInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'put', + path: INVENTORY_VIEW_URL_ENTITY, + validate: { + params: createValidationFunction(inventoryViewRequestParamsRT), + query: createValidationFunction(inventoryViewRequestQueryRT), + body: createValidationFunction(updateInventoryViewRequestPayloadRT), + }, + }, + async (_requestContext, request, response) => { + const { body, params, query } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + const inventoryView = await inventoryViewsClient.update( + params.inventoryViewId, + body.attributes, + query + ); + + return response.ok({ + body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }), + }); + } catch (error) { + if (isBoom(error)) { + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/saved_objects/index.ts b/x-pack/plugins/infra/server/saved_objects/index.ts index bd7ecac5179a15..cf6906fc733f73 100644 --- a/x-pack/plugins/infra/server/saved_objects/index.ts +++ b/x-pack/plugins/infra/server/saved_objects/index.ts @@ -5,4 +5,6 @@ * 2.0. */ +export * from './inventory_view'; export * from './log_view'; +export * from './metrics_explorer_view'; diff --git a/x-pack/plugins/infra/server/saved_objects/inventory_view/index.ts b/x-pack/plugins/infra/server/saved_objects/inventory_view/index.ts new file mode 100644 index 00000000000000..458d3fa65c6a05 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/inventory_view/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + inventoryViewSavedObjectName, + inventoryViewSavedObjectType, +} from './inventory_view_saved_object'; +export { inventoryViewSavedObjectRT } from './types'; diff --git a/x-pack/plugins/infra/server/saved_objects/inventory_view/inventory_view_saved_object.ts b/x-pack/plugins/infra/server/saved_objects/inventory_view/inventory_view_saved_object.ts new file mode 100644 index 00000000000000..f9c4c4d3540241 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/inventory_view/inventory_view_saved_object.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import type { SavedObject, SavedObjectsType } from '@kbn/core/server'; +import { inventoryViewSavedObjectRT } from './types'; + +export const inventoryViewSavedObjectName = 'inventory-view'; + +const getInventoryViewTitle = (savedObject: SavedObject) => + pipe( + inventoryViewSavedObjectRT.decode(savedObject), + fold( + () => `Inventory view [id=${savedObject.id}]`, + ({ attributes: { name } }) => name + ) + ); + +export const inventoryViewSavedObjectType: SavedObjectsType = { + name: inventoryViewSavedObjectName, + hidden: false, + namespaceType: 'single', + management: { + defaultSearchField: 'name', + displayName: 'inventory view', + getTitle: getInventoryViewTitle, + icon: 'metricsApp', + importableAndExportable: true, + }, + mappings: { + dynamic: false, + properties: {}, + }, +}; diff --git a/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts b/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts new file mode 100644 index 00000000000000..45e738f3920f15 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isoToEpochRt, nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const inventoryViewSavedObjectAttributesRT = rt.intersection([ + rt.strict({ + name: nonEmptyStringRt, + }), + rt.UnknownRecord, +]); + +export const inventoryViewSavedObjectRT = rt.intersection([ + rt.type({ + id: rt.string, + attributes: inventoryViewSavedObjectAttributesRT, + }), + rt.partial({ + version: rt.string, + updated_at: isoToEpochRt, + }), +]); diff --git a/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/index.ts b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/index.ts new file mode 100644 index 00000000000000..6f3f926319cf2f --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + metricsExplorerViewSavedObjectName, + metricsExplorerViewSavedObjectType, +} from './metrics_explorer_view_saved_object'; +export { metricsExplorerViewSavedObjectRT } from './types'; diff --git a/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/metrics_explorer_view_saved_object.ts b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/metrics_explorer_view_saved_object.ts new file mode 100644 index 00000000000000..ce47aa93951b66 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/metrics_explorer_view_saved_object.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import type { SavedObject, SavedObjectsType } from '@kbn/core/server'; +import { metricsExplorerViewSavedObjectRT } from './types'; + +export const metricsExplorerViewSavedObjectName = 'metrics-explorer-view'; + +const getMetricsExplorerViewTitle = (savedObject: SavedObject) => + pipe( + metricsExplorerViewSavedObjectRT.decode(savedObject), + fold( + () => `Metrics explorer view [id=${savedObject.id}]`, + ({ attributes: { name } }) => name + ) + ); + +export const metricsExplorerViewSavedObjectType: SavedObjectsType = { + name: metricsExplorerViewSavedObjectName, + hidden: false, + namespaceType: 'single', + management: { + defaultSearchField: 'name', + displayName: 'metrics explorer view', + getTitle: getMetricsExplorerViewTitle, + icon: 'metricsApp', + importableAndExportable: true, + }, + mappings: { + dynamic: false, + properties: {}, + }, +}; diff --git a/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/types.ts b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/types.ts new file mode 100644 index 00000000000000..1168b2003994e8 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isoToEpochRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; +import { metricsExplorerViewAttributesRT } from '../../../common/metrics_explorer_views'; + +export const metricsExplorerViewSavedObjectRT = rt.intersection([ + rt.type({ + id: rt.string, + attributes: metricsExplorerViewAttributesRT, + }), + rt.partial({ + version: rt.string, + updated_at: isoToEpochRt, + }), +]); diff --git a/x-pack/plugins/infra/server/services/inventory_views/index.ts b/x-pack/plugins/infra/server/services/inventory_views/index.ts new file mode 100644 index 00000000000000..1df6b8cd448148 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { InventoryViewsService } from './inventory_views_service'; +export { InventoryViewsClient } from './inventory_views_client'; +export type { + InventoryViewsServiceSetup, + InventoryViewsServiceStart, + InventoryViewsServiceStartDeps, +} from './types'; diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts new file mode 100644 index 00000000000000..9d832f8502104f --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IInventoryViewsClient } from './types'; + +export const createInventoryViewsClientMock = (): jest.Mocked => ({ + delete: jest.fn(), + find: jest.fn(), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), +}); diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.test.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.test.ts new file mode 100644 index 00000000000000..5d5b253045de45 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.test.ts @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggerMock } from '@kbn/logging-mocks'; +import { SavedObjectsClientContract } from '@kbn/core/server'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { InventoryViewAttributes } from '../../../common/inventory_views'; + +import { InfraSource } from '../../lib/sources'; +import { createInfraSourcesMock } from '../../lib/sources/mocks'; +import { inventoryViewSavedObjectName } from '../../saved_objects/inventory_view'; +import { InventoryViewsClient } from './inventory_views_client'; +import { createInventoryViewMock } from '../../../common/inventory_views/inventory_view.mock'; +import { + CreateInventoryViewAttributesRequestPayload, + UpdateInventoryViewAttributesRequestPayload, +} from '../../../common/http_api/latest'; + +describe('InventoryViewsClient class', () => { + const mockFindInventoryList = (savedObjectsClient: jest.Mocked) => { + const inventoryViewListMock = [ + createInventoryViewMock('0', { + isDefault: true, + } as InventoryViewAttributes), + createInventoryViewMock('default_id', { + name: 'Default view 2', + isStatic: false, + } as InventoryViewAttributes), + createInventoryViewMock('custom_id', { + name: 'Custom', + isStatic: false, + } as InventoryViewAttributes), + ]; + + savedObjectsClient.find.mockResolvedValue({ + total: 2, + saved_objects: inventoryViewListMock.slice(1).map((view) => ({ + ...view, + type: inventoryViewSavedObjectName, + score: 0, + references: [], + })), + per_page: 1000, + page: 1, + }); + + return inventoryViewListMock; + }; + + describe('.find', () => { + it('resolves the list of existing inventory views', async () => { + const { inventoryViewsClient, infraSources, savedObjectsClient } = + createInventoryViewsClient(); + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + const inventoryViewListMock = mockFindInventoryList(savedObjectsClient); + + const inventoryViewList = await inventoryViewsClient.find({}); + + expect(savedObjectsClient.find).toHaveBeenCalled(); + expect(inventoryViewList).toEqual(inventoryViewListMock); + }); + + it('always resolves at least the static inventory view', async () => { + const { inventoryViewsClient, infraSources, savedObjectsClient } = + createInventoryViewsClient(); + + const inventoryViewListMock = [ + createInventoryViewMock('0', { + isDefault: true, + } as InventoryViewAttributes), + ]; + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + savedObjectsClient.find.mockResolvedValue({ + total: 2, + saved_objects: [], + per_page: 1000, + page: 1, + }); + + const inventoryViewList = await inventoryViewsClient.find({}); + + expect(savedObjectsClient.find).toHaveBeenCalled(); + expect(inventoryViewList).toEqual(inventoryViewListMock); + }); + }); + + it('.get resolves the an inventory view by id', async () => { + const { inventoryViewsClient, infraSources, savedObjectsClient } = createInventoryViewsClient(); + + const inventoryViewMock = createInventoryViewMock('custom_id', { + name: 'Custom', + isDefault: false, + isStatic: false, + } as InventoryViewAttributes); + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + savedObjectsClient.get.mockResolvedValue({ + ...inventoryViewMock, + type: inventoryViewSavedObjectName, + references: [], + }); + + const inventoryView = await inventoryViewsClient.get('custom_id', {}); + + expect(savedObjectsClient.get).toHaveBeenCalled(); + expect(inventoryView).toEqual(inventoryViewMock); + }); + + describe('.create', () => { + it('generate a new inventory view', async () => { + const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); + + const inventoryViewMock = createInventoryViewMock('new_id', { + name: 'New view', + isStatic: false, + } as InventoryViewAttributes); + + mockFindInventoryList(savedObjectsClient); + + savedObjectsClient.create.mockResolvedValue({ + ...inventoryViewMock, + type: inventoryViewSavedObjectName, + references: [], + }); + + const inventoryView = await inventoryViewsClient.create({ + name: 'New view', + } as CreateInventoryViewAttributesRequestPayload); + + expect(savedObjectsClient.create).toHaveBeenCalled(); + expect(inventoryView).toEqual(inventoryViewMock); + }); + + it('throws an error when a conflicting name is given', async () => { + const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); + + mockFindInventoryList(savedObjectsClient); + + await expect( + async () => + await inventoryViewsClient.create({ + name: 'Custom', + } as CreateInventoryViewAttributesRequestPayload) + ).rejects.toThrow('A view with that name already exists.'); + }); + }); + + describe('.update', () => { + it('update an existing inventory view by id', async () => { + const { inventoryViewsClient, infraSources, savedObjectsClient } = + createInventoryViewsClient(); + + const inventoryViews = mockFindInventoryList(savedObjectsClient); + + const inventoryViewMock = { + ...inventoryViews[1], + attributes: { + ...inventoryViews[1].attributes, + name: 'New name', + }, + }; + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + savedObjectsClient.update.mockResolvedValue({ + ...inventoryViewMock, + type: inventoryViewSavedObjectName, + references: [], + }); + + const inventoryView = await inventoryViewsClient.update( + 'default_id', + { + name: 'New name', + } as UpdateInventoryViewAttributesRequestPayload, + {} + ); + + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(inventoryView).toEqual(inventoryViewMock); + }); + + it('throws an error when a conflicting name is given', async () => { + const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); + + mockFindInventoryList(savedObjectsClient); + + await expect( + async () => + await inventoryViewsClient.update( + 'default_id', + { + name: 'Custom', + } as UpdateInventoryViewAttributesRequestPayload, + {} + ) + ).rejects.toThrow('A view with that name already exists.'); + }); + }); + + it('.delete removes an inventory view by id', async () => { + const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); + + savedObjectsClient.delete.mockResolvedValue({}); + + const inventoryView = await inventoryViewsClient.delete('custom_id'); + + expect(savedObjectsClient.delete).toHaveBeenCalled(); + expect(inventoryView).toEqual({}); + }); +}); + +const createInventoryViewsClient = () => { + const logger = loggerMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); + const infraSources = createInfraSourcesMock(); + + const inventoryViewsClient = new InventoryViewsClient(logger, savedObjectsClient, infraSources); + + return { + infraSources, + inventoryViewsClient, + savedObjectsClient, + }; +}; + +const basicTestSourceConfiguration: InfraSource = { + id: 'ID', + origin: 'stored', + configuration: { + name: 'NAME', + description: 'DESCRIPTION', + logIndices: { + type: 'index_pattern', + indexPatternId: 'INDEX_PATTERN_ID', + }, + logColumns: [], + fields: { + message: [], + }, + metricAlias: 'METRIC_ALIAS', + inventoryDefaultView: '0', + metricsExplorerDefaultView: 'METRICS_EXPLORER_DEFAULT_VIEW', + anomalyThreshold: 0, + }, +}; diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts new file mode 100644 index 00000000000000..55a8df1024a6e8 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + Logger, + SavedObject, + SavedObjectsClientContract, + SavedObjectsUpdateResponse, +} from '@kbn/core/server'; +import Boom from '@hapi/boom'; +import { + staticInventoryViewAttributes, + staticInventoryViewId, +} from '../../../common/inventory_views'; +import type { + CreateInventoryViewAttributesRequestPayload, + InventoryViewRequestQuery, +} from '../../../common/http_api/latest'; +import type { InventoryView, InventoryViewAttributes } from '../../../common/inventory_views'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import type { IInfraSources } from '../../lib/sources'; +import { inventoryViewSavedObjectName } from '../../saved_objects/inventory_view'; +import { inventoryViewSavedObjectRT } from '../../saved_objects/inventory_view/types'; +import type { IInventoryViewsClient } from './types'; + +export class InventoryViewsClient implements IInventoryViewsClient { + constructor( + private readonly logger: Logger, + private readonly savedObjectsClient: SavedObjectsClientContract, + private readonly infraSources: IInfraSources + ) {} + + static STATIC_VIEW_ID = '0'; + + public async find(query: InventoryViewRequestQuery): Promise { + this.logger.debug('Trying to load inventory views ...'); + + const sourceId = query.sourceId ?? 'default'; + + const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([ + this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId), + this.savedObjectsClient.find({ + type: inventoryViewSavedObjectName, + perPage: 1000, // Fetch 1 page by default with a max of 1000 results + }), + ]); + + const defaultView = InventoryViewsClient.createStaticView( + sourceConfiguration.configuration.inventoryDefaultView + ); + const views = inventoryViewSavedObject.saved_objects.map((savedObject) => + this.mapSavedObjectToInventoryView( + savedObject, + sourceConfiguration.configuration.inventoryDefaultView + ) + ); + + const inventoryViews = [defaultView, ...views]; + + const sortedInventoryViews = this.moveDefaultViewOnTop(inventoryViews); + + return sortedInventoryViews; + } + + public async get( + inventoryViewId: string, + query: InventoryViewRequestQuery + ): Promise { + this.logger.debug(`Trying to load inventory view with id ${inventoryViewId} ...`); + + const sourceId = query.sourceId ?? 'default'; + + // Handle the case where the requested resource is the static inventory view + if (inventoryViewId === InventoryViewsClient.STATIC_VIEW_ID) { + const sourceConfiguration = await this.infraSources.getSourceConfiguration( + this.savedObjectsClient, + sourceId + ); + + return InventoryViewsClient.createStaticView( + sourceConfiguration.configuration.inventoryDefaultView + ); + } + + const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([ + this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId), + this.savedObjectsClient.get(inventoryViewSavedObjectName, inventoryViewId), + ]); + + return this.mapSavedObjectToInventoryView( + inventoryViewSavedObject, + sourceConfiguration.configuration.inventoryDefaultView + ); + } + + public async create( + attributes: CreateInventoryViewAttributesRequestPayload + ): Promise { + this.logger.debug(`Trying to create inventory view ...`); + + // Validate there is not a view with the same name + await this.assertNameConflict(attributes.name); + + const inventoryViewSavedObject = await this.savedObjectsClient.create( + inventoryViewSavedObjectName, + attributes + ); + + return this.mapSavedObjectToInventoryView(inventoryViewSavedObject); + } + + public async update( + inventoryViewId: string, + attributes: CreateInventoryViewAttributesRequestPayload, + query: InventoryViewRequestQuery + ): Promise { + this.logger.debug(`Trying to update inventory view with id "${inventoryViewId}"...`); + + // Validate there is not a view with the same name + await this.assertNameConflict(attributes.name, [inventoryViewId]); + + const sourceId = query.sourceId ?? 'default'; + + const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([ + this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId), + this.savedObjectsClient.update(inventoryViewSavedObjectName, inventoryViewId, attributes), + ]); + + return this.mapSavedObjectToInventoryView( + inventoryViewSavedObject, + sourceConfiguration.configuration.inventoryDefaultView + ); + } + + public delete(inventoryViewId: string): Promise<{}> { + this.logger.debug(`Trying to delete inventory view with id ${inventoryViewId} ...`); + + return this.savedObjectsClient.delete(inventoryViewSavedObjectName, inventoryViewId); + } + + private mapSavedObjectToInventoryView( + savedObject: SavedObject | SavedObjectsUpdateResponse, + defaultViewId?: string + ) { + const inventoryViewSavedObject = decodeOrThrow(inventoryViewSavedObjectRT)(savedObject); + + return { + id: inventoryViewSavedObject.id, + version: inventoryViewSavedObject.version, + updatedAt: inventoryViewSavedObject.updated_at, + attributes: { + ...inventoryViewSavedObject.attributes, + isDefault: inventoryViewSavedObject.id === defaultViewId, + isStatic: false, + }, + }; + } + + private moveDefaultViewOnTop(views: InventoryView[]) { + const defaultViewPosition = views.findIndex((view) => view.attributes.isDefault); + + if (defaultViewPosition !== -1) { + const element = views.splice(defaultViewPosition, 1)[0]; + views.unshift(element); + } + + return views; + } + + /** + * We want to control conflicting names on the views + */ + private async assertNameConflict(name: string, whitelist: string[] = []) { + const results = await this.savedObjectsClient.find({ + type: inventoryViewSavedObjectName, + perPage: 1000, + }); + + const hasConflict = [InventoryViewsClient.createStaticView(), ...results.saved_objects].some( + (obj) => !whitelist.includes(obj.id) && obj.attributes.name === name + ); + + if (hasConflict) { + throw Boom.conflict('A view with that name already exists.'); + } + } + + private static createStaticView = (defaultViewId?: string): InventoryView => ({ + id: staticInventoryViewId, + attributes: { + ...staticInventoryViewAttributes, + isDefault: defaultViewId === InventoryViewsClient.STATIC_VIEW_ID, + }, + }); +} diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.mock.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.mock.ts new file mode 100644 index 00000000000000..cb3e85643303c5 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.mock.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createInventoryViewsClientMock } from './inventory_views_client.mock'; +import type { InventoryViewsServiceSetup, InventoryViewsServiceStart } from './types'; + +export const createInventoryViewsServiceSetupMock = + (): jest.Mocked => {}; + +export const createInventoryViewsServiceStartMock = + (): jest.Mocked => ({ + getClient: jest.fn((_savedObjectsClient: any) => createInventoryViewsClientMock()), + getScopedClient: jest.fn((_request: any) => createInventoryViewsClientMock()), + }); diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.ts new file mode 100644 index 00000000000000..3f51f0e65b29cb --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import { InventoryViewsClient } from './inventory_views_client'; +import type { + InventoryViewsServiceSetup, + InventoryViewsServiceStart, + InventoryViewsServiceStartDeps, +} from './types'; + +export class InventoryViewsService { + constructor(private readonly logger: Logger) {} + + public setup(): InventoryViewsServiceSetup {} + + public start({ + infraSources, + savedObjects, + }: InventoryViewsServiceStartDeps): InventoryViewsServiceStart { + const { logger } = this; + + return { + getClient(savedObjectsClient: SavedObjectsClientContract) { + return new InventoryViewsClient(logger, savedObjectsClient, infraSources); + }, + + getScopedClient(request: KibanaRequest) { + const savedObjectsClient = savedObjects.getScopedClient(request); + + return this.getClient(savedObjectsClient); + }, + }; + } +} diff --git a/x-pack/plugins/infra/server/services/inventory_views/types.ts b/x-pack/plugins/infra/server/services/inventory_views/types.ts new file mode 100644 index 00000000000000..3e023b77af6c26 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + KibanaRequest, + SavedObjectsClientContract, + SavedObjectsServiceStart, +} from '@kbn/core/server'; +import type { + CreateInventoryViewAttributesRequestPayload, + InventoryViewRequestQuery, + UpdateInventoryViewAttributesRequestPayload, +} from '../../../common/http_api/latest'; +import type { InventoryView } from '../../../common/inventory_views'; +import type { InfraSources } from '../../lib/sources'; + +export interface InventoryViewsServiceStartDeps { + infraSources: InfraSources; + savedObjects: SavedObjectsServiceStart; +} + +export type InventoryViewsServiceSetup = void; + +export interface InventoryViewsServiceStart { + getClient(savedObjectsClient: SavedObjectsClientContract): IInventoryViewsClient; + getScopedClient(request: KibanaRequest): IInventoryViewsClient; +} + +export interface IInventoryViewsClient { + delete(inventoryViewId: string): Promise<{}>; + find(query: InventoryViewRequestQuery): Promise; + get(inventoryViewId: string, query: InventoryViewRequestQuery): Promise; + create( + inventoryViewAttributes: CreateInventoryViewAttributesRequestPayload + ): Promise; + update( + inventoryViewId: string, + inventoryViewAttributes: UpdateInventoryViewAttributesRequestPayload, + query: InventoryViewRequestQuery + ): Promise; +} diff --git a/x-pack/plugins/infra/server/types.ts b/x-pack/plugins/infra/server/types.ts index 108575c0f83249..c415103d2256de 100644 --- a/x-pack/plugins/infra/server/types.ts +++ b/x-pack/plugins/infra/server/types.ts @@ -14,6 +14,7 @@ import type { SearchRequestHandlerContext } from '@kbn/data-plugin/server'; import type { MlPluginSetup } from '@kbn/ml-plugin/server'; import type { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; import { InfraServerPluginStartDeps } from './lib/adapters/framework'; +import { InventoryViewsServiceStart } from './services/inventory_views'; import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views/types'; export type { InfraConfig } from '../common/plugin_config_types'; @@ -30,6 +31,7 @@ export interface InfraPluginSetup { } export interface InfraPluginStart { + inventoryViews: InventoryViewsServiceStart; logViews: LogViewsServiceStart; getMetricIndices: ( savedObjectsClient: SavedObjectsClientContract, From 6fe3a674a790dd686c92a311fbf7aa10647dc417 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 24 Apr 2023 11:58:56 +0200 Subject: [PATCH 35/65] [NewsFeed] Remove React warning for flyout list (#155571) ## Summary Fixes: https://github.com/elastic/kibana/issues/154923 The problem was the `showPlainSpinner` was passed as part of the props to `EuiFlyout` component, which was trying to pass it as a DOM attribute. Removing this from props gets rid of the warning. Would be nice if we could get this in before 8.8 FF. xoxo cc @Dosant @rshen91 ### Checklist Delete any items that are not applicable to this PR. ~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)~ ~- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials~ ~- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios~ ~- [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))~ ~- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))~ ~- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~ ~- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))~ ~- [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)~ ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- src/plugins/newsfeed/public/components/flyout_list.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/newsfeed/public/components/flyout_list.tsx b/src/plugins/newsfeed/public/components/flyout_list.tsx index 23be44230e2207..c09524d384e868 100644 --- a/src/plugins/newsfeed/public/components/flyout_list.tsx +++ b/src/plugins/newsfeed/public/components/flyout_list.tsx @@ -32,11 +32,11 @@ import { NewsLoadingPrompt } from './loading_news'; export const NewsfeedFlyout = (props: Partial & { showPlainSpinner: boolean }) => { const { newsFetchResult, setFlyoutVisible } = useContext(NewsfeedContext); const closeFlyout = useCallback(() => setFlyoutVisible(false), [setFlyoutVisible]); - + const { showPlainSpinner, ...rest } = props; return ( Date: Mon, 24 Apr 2023 12:17:37 +0200 Subject: [PATCH 36/65] [Infrastructure UI] Hosts view remove flyout state from the url after the flyout is closed (#155317) Closes #154564 ## Summary This PR removes the flyout state from the URL if the flyout is closed and set the default state if the flyout is open again ## Testing - Open a single host flyout and change the tab/add filter or search (do not close the flyout yet) - Copy the URL and verify that you see the same flyout data in a new browser tab/window - Close the flyout - Check the URL (the flyout state should not be there) - Copy the URL and verify that you see the flyout data is not there (flyout is still closed and when it's open it has default state (metadata page open) - if there are any metadata filters applied they should still be part of the unified search bar https://user-images.githubusercontent.com/14139027/233397203-d99fc04a-a118-43f8-a43b-6b01d34ab3b8.mov --- .../hooks/use_host_flyout_open_url_state.ts | 31 +++++++++++++------ .../metrics/hosts/hooks/use_hosts_table.tsx | 8 ++--- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_open_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_open_url_state.ts index 635fe556e85e50..663ecf3a92643c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_open_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_open_url_state.ts @@ -25,19 +25,29 @@ export const GET_DEFAULT_TABLE_PROPERTIES = { const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'hostFlyoutOpen'; type Action = rt.TypeOf; -type SetNewHostFlyoutOpen = (newProps: Action) => void; - -export const useHostFlyoutOpen = (): [HostFlyoutOpen, SetNewHostFlyoutOpen] => { - const [urlState, setUrlState] = useUrlState({ - defaultState: GET_DEFAULT_TABLE_PROPERTIES, +type SetNewHostFlyoutOpen = (newProp: Action) => void; +type SetNewHostFlyoutClose = () => void; + +export const useHostFlyoutOpen = (): [ + HostFlyoutOpen, + SetNewHostFlyoutOpen, + SetNewHostFlyoutClose +] => { + const [urlState, setUrlState] = useUrlState({ + defaultState: '', decodeUrlState, encodeUrlState, urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, }); - const setHostFlyoutOpen = (newProps: Action) => setUrlState({ ...urlState, ...newProps }); + const setHostFlyoutOpen = (newProps: Action) => + typeof urlState !== 'string' + ? setUrlState({ ...urlState, ...newProps }) + : setUrlState({ ...GET_DEFAULT_TABLE_PROPERTIES, ...newProps }); + + const setFlyoutClosed = () => setUrlState(''); - return [urlState, setHostFlyoutOpen]; + return [urlState as HostFlyoutOpen, setHostFlyoutOpen, setFlyoutClosed]; }; const FlyoutTabIdRT = rt.union([rt.literal('metadata'), rt.literal('processes')]); @@ -74,9 +84,12 @@ const HostFlyoutOpenRT = rt.type({ metadataSearch: SearchFilterRT, }); +const HostFlyoutUrlRT = rt.union([HostFlyoutOpenRT, rt.string]); + +type HostFlyoutUrl = rt.TypeOf; type HostFlyoutOpen = rt.TypeOf; -const encodeUrlState = HostFlyoutOpenRT.encode; +const encodeUrlState = HostFlyoutUrlRT.encode; const decodeUrlState = (value: unknown) => { - return pipe(HostFlyoutOpenRT.decode(value), fold(constant(undefined), identity)); + return pipe(HostFlyoutUrlRT.decode(value), fold(constant('undefined'), identity)); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 0b7021b97f84ce..44a492f314c1ce 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -125,9 +125,9 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) services: { telemetry }, } = useKibanaContextForPlugin(); - const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen(); + const [hostFlyoutOpen, setHostFlyoutOpen, setFlyoutClosed] = useHostFlyoutOpen(); - const closeFlyout = () => setHostFlyoutOpen({ clickedItemId: '' }); + const closeFlyout = () => setFlyoutClosed(); const reportHostEntryClick = useCallback( ({ name, cloudProvider }: HostNodeRow['title']) => { @@ -166,7 +166,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) clickedItemId: id, }); if (id === hostFlyoutOpen.clickedItemId) { - setHostFlyoutOpen({ clickedItemId: '' }); + setFlyoutClosed(); } else { setHostFlyoutOpen({ clickedItemId: id }); } @@ -244,7 +244,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) align: 'right', }, ], - [hostFlyoutOpen.clickedItemId, reportHostEntryClick, setHostFlyoutOpen, time] + [hostFlyoutOpen.clickedItemId, reportHostEntryClick, setFlyoutClosed, setHostFlyoutOpen, time] ); return { From d0642f75d84b10f02ba3628459a14ce0bfc1dbf0 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 24 Apr 2023 12:50:10 +0100 Subject: [PATCH 37/65] [Security Solution] Risk score area chart displayed error (#155597) ## Summary Risk score area chart displayed error on host details page: /app/security/hosts/name/{hostName}/hostRisk Before: ![Screenshot 2023-04-24 at 11 02 19](https://user-images.githubusercontent.com/6295984/233961792-7f8c64f6-0bbb-467d-b762-0b9cbf86a979.png) After: Screenshot 2023-04-24 at 10 21 55 Relevant changes: https://github.com/elastic/kibana/pull/152506/files#diff-260dbbbd965ff10673ef7670e2cb819173662b51bb353b7ad702a60334e874ff ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../risk_score_over_time_area.test.ts.snap | 2 +- .../risk_score_over_time_area.test.ts | 39 +++++++++++++++++++ .../risk_scores/risk_score_over_time_area.ts | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/__snapshots__/risk_score_over_time_area.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/__snapshots__/risk_score_over_time_area.test.ts.snap index b6177143f024c9..8a37b8b9fe54b6 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/__snapshots__/risk_score_over_time_area.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/__snapshots__/risk_score_over_time_area.test.ts.snap @@ -184,7 +184,7 @@ Object { "color": "#aa6556", "fill": "none", "forAccessor": "1dd5663b-f062-43f8-8688-fc8166c2ca8e", - "icon": "warning", + "icon": "alert", "iconPosition": "left", "lineWidth": 2, "textVisibility": true, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.test.ts index 08cd3d131d166c..97dbe4ea17e48b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.test.ts @@ -6,6 +6,7 @@ */ import { renderHook } from '@testing-library/react-hooks'; +import type { XYState } from '@kbn/lens-plugin/public'; import { wrapper } from '../../../mocks'; import { useLensAttributes } from '../../../use_lens_attributes'; @@ -56,4 +57,42 @@ describe('getRiskScoreOverTimeAreaAttributes', () => { expect(result?.current).toMatchSnapshot(); }); + + it('should render a Reference Line with an Alert icon', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getRiskScoreOverTimeAreaAttributes, + stackByField: 'host', + extraOptions: { + spaceId: 'mockSpaceId', + }, + }), + { wrapper } + ); + + expect( + (result?.current?.state.visualization as XYState).layers.find( + (layer) => layer.layerType === 'referenceLine' + ) + ).toEqual( + expect.objectContaining({ + layerId: '1dd5663b-f062-43f8-8688-fc8166c2ca8e', + layerType: 'referenceLine', + accessors: ['1dd5663b-f062-43f8-8688-fc8166c2ca8e'], + yConfig: [ + { + forAccessor: '1dd5663b-f062-43f8-8688-fc8166c2ca8e', + axisMode: 'left', + lineWidth: 2, + color: '#aa6556', + icon: 'alert', + textVisibility: true, + fill: 'none', + iconPosition: 'left', + }, + ], + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.ts index 8c5981d6e4a069..b100e5042a33a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.ts @@ -56,7 +56,7 @@ export const getRiskScoreOverTimeAreaAttributes: GetLensAttributes = ( axisMode: 'left', lineWidth: 2, color: '#aa6556', - icon: 'warning', + icon: 'alert', textVisibility: true, fill: 'none', iconPosition: 'left', From 792c7868b1ab056ecf00890847c6558574753c08 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Mon, 24 Apr 2023 13:51:05 +0200 Subject: [PATCH 38/65] [Defend Workflows][E2E] Isolate command e2e coverage (#154465) E2E coverage of TestRail "Isolate" suite. These tests are run against mocked documents. We intercept REST request whenever `Isolate` button is used and mock `response action response` with `success`. --- .../user_actions/comment/actions.tsx | 4 +- .../endpoint_metadata_generator.ts | 9 +- .../endpoint_rule_alert_generator.ts | 2 + .../data_loaders/index_endpoint_hosts.ts | 19 +- .../index_endpoint_rule_alerts.ts | 12 +- .../common/endpoint/generate_data.ts | 4 +- .../common/endpoint/index_data.ts | 5 +- .../public/management/cypress/cypress.d.ts | 13 +- .../cypress/e2e/mocked_data/endpoints.cy.ts | 17 +- .../cypress/e2e/mocked_data/isolate.cy.ts | 328 ++++++++++++++++++ .../cypress/support/data_loaders.ts | 19 +- .../plugin_handlers/endpoint_data_loader.ts | 10 +- .../tasks/index_endpoint_rule_alerts.ts | 2 + .../management/cypress/tasks/isolate.ts | 69 ++++ .../public/management/cypress/types.ts | 13 +- .../endpoint_hosts/store/middleware.test.ts | 18 +- .../services/endpoint_response_actions.ts | 14 +- .../apps/endpoint/endpoint_list.ts | 16 +- 18 files changed, 526 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx index 5b17b05a45f68c..dccf5ae0b91a28 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx @@ -34,6 +34,7 @@ export const createActionAttachmentUserActionBuilder = ({ // TODO: Fix this manually. Issue #123375 // eslint-disable-next-line react/display-name build: () => { + const actionIconName = comment.actions.type === 'isolate' ? 'lock' : 'lockOpen'; return [ { username: ( @@ -52,7 +53,8 @@ export const createActionAttachmentUserActionBuilder = ({ ), 'data-test-subj': 'endpoint-action', timestamp: , - timelineAvatar: comment.actions.type === 'isolate' ? 'lock' : 'lockOpen', + timelineAvatar: actionIconName, + timelineAvatarAriaLabel: actionIconName, actions: , children: comment.comment.trim().length > 0 && ( diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts index e5a44c46f5afc1..bc429feb208d7a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts @@ -20,6 +20,7 @@ export interface GetCustomEndpointMetadataGeneratorOptions { version: string; /** OS type for the generated endpoint hosts */ os: 'macOS' | 'windows' | 'linux'; + isolation: boolean; } /** @@ -33,6 +34,7 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { static custom({ version, os, + isolation, }: Partial = {}): typeof EndpointMetadataGenerator { return class extends EndpointMetadataGenerator { generate(overrides: DeepPartial = {}): HostMetadataInterface { @@ -54,6 +56,9 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { set(overrides, 'host.os', EndpointMetadataGenerator.windowsOSFields); } } + if (isolation !== undefined) { + set(overrides, 'Endpoint.state.isolation', isolation); + } return super.generate(overrides); } @@ -104,10 +109,10 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { /** Generate an Endpoint host metadata document */ generate(overrides: DeepPartial = {}): HostMetadataInterface { const ts = overrides['@timestamp'] ?? new Date().getTime(); - const hostName = this.randomHostname(); + const hostName = overrides?.host?.hostname ?? this.randomHostname(); const agentVersion = overrides?.agent?.version ?? this.randomVersion(); const agentId = this.seededUUIDv4(); - const isIsolated = this.randomBoolean(0.3); + const isIsolated = overrides?.Endpoint?.state?.isolation ?? this.randomBoolean(0.3); const capabilities: EndpointCapabilities[] = ['isolation']; // v8.4 introduced additional endpoint capabilities diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts index 5578c179ba1f56..8396f86a45e971 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts @@ -37,6 +37,8 @@ export class EndpointRuleAlertGenerator extends BaseDataGenerator { const endpointMetadataGenerator = new EndpointMetadataGenerator(); const endpointMetadata = endpointMetadataGenerator.generate({ agent: { version: kibanaPackageJson.version }, + host: { hostname: overrides?.host?.hostname }, + Endpoint: { state: { isolation: overrides?.Endpoint?.state?.isolation } }, }); const now = overrides['@timestamp'] ?? new Date().toISOString(); const endpointAgentId = overrides?.agent?.id ?? this.seededUUIDv4(); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index f9f96e650c0565..684694bdb5c9ab 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -75,6 +75,7 @@ export interface IndexedHostsResponse * @param policyResponseIndex * @param enrollFleet * @param generator + * @param disableEndpointActionsForHost */ export async function indexEndpointHostDocs({ numDocs, @@ -86,6 +87,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex, enrollFleet, generator, + withResponseActions = true, }: { numDocs: number; client: Client; @@ -96,6 +98,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex: string; enrollFleet: boolean; generator: EndpointDocGenerator; + withResponseActions?: boolean; }): Promise { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); @@ -190,13 +193,15 @@ export async function indexEndpointHostDocs({ }, }; - // Create some fleet endpoint actions and .logs-endpoint actions for this Host - const actionsResponse = await indexEndpointAndFleetActionsForHost( - client, - hostMetadata, - undefined - ); - mergeAndAppendArrays(response, actionsResponse); + if (withResponseActions) { + // Create some fleet endpoint actions and .logs-endpoint actions for this Host + const actionsResponse = await indexEndpointAndFleetActionsForHost( + client, + hostMetadata, + undefined + ); + mergeAndAppendArrays(response, actionsResponse); + } } await client diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts index 74e9d82a714e9c..1c5883c0521352 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts @@ -22,6 +22,8 @@ import { EndpointRuleAlertGenerator } from '../data_generators/endpoint_rule_ale export interface IndexEndpointRuleAlertsOptions { esClient: Client; endpointAgentId: string; + endpointHostname?: string; + endpointIsolated?: boolean; count?: number; log?: ToolingLog; } @@ -40,12 +42,16 @@ export interface DeletedIndexedEndpointRuleAlerts { * written them to for a given endpoint * @param esClient * @param endpointAgentId + * @param endpointHostname + * @param endpointIsolated * @param count * @param log */ export const indexEndpointRuleAlerts = async ({ esClient, endpointAgentId, + endpointHostname, + endpointIsolated, count = 1, log = new ToolingLog(), }: IndexEndpointRuleAlertsOptions): Promise => { @@ -57,7 +63,11 @@ export const indexEndpointRuleAlerts = async ({ const indexedAlerts: estypes.IndexResponse[] = []; for (let n = 0; n < count; n++) { - const alert = alertsGenerator.generate({ agent: { id: endpointAgentId } }); + const alert = alertsGenerator.generate({ + agent: { id: endpointAgentId }, + host: { hostname: endpointHostname }, + ...(endpointIsolated ? { Endpoint: { state: { isolation: endpointIsolated } } } : {}), + }); const indexedAlert = await esClient.index({ index: `${DEFAULT_ALERTS_INDEX}-default`, refresh: 'wait_for', diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index d867c25ececf7c..76ee903eb6889f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -410,7 +410,9 @@ export class EndpointDocGenerator extends BaseDataGenerator { private createHostData(): CommonHostInfo { const { agent, elastic, host, Endpoint } = this.metadataGenerator.generate({ - Endpoint: { policy: { applied: this.randomChoice(APPLIED_POLICIES) } }, + Endpoint: { + policy: { applied: this.randomChoice(APPLIED_POLICIES) }, + }, }); return { agent, elastic, host, Endpoint }; diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 77b3135c123531..db5039e5a72f03 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -48,6 +48,7 @@ export type IndexedHostsAndAlertsResponse = IndexedHostsResponse; * @param fleet * @param options * @param DocGenerator + * @param withResponseActions */ export async function indexHostsAndAlerts( client: Client, @@ -62,7 +63,8 @@ export async function indexHostsAndAlerts( alertsPerHost: number, fleet: boolean, options: TreeOptions = {}, - DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator + DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator, + withResponseActions = true ): Promise { const random = seedrandom(seed); const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); @@ -114,6 +116,7 @@ export async function indexHostsAndAlerts( policyResponseIndex, enrollFleet: fleet, generator, + withResponseActions, }); mergeAndAppendArrays(response, indexedHosts); diff --git a/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts b/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts index a48498a7ee43b9..c461f712b75b57 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts @@ -11,8 +11,11 @@ import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; import type { IndexedEndpointPolicyResponse } from '../../../common/endpoint/data_loaders/index_endpoint_policy_response'; -import type { HostPolicyResponse } from '../../../common/endpoint/types'; -import type { IndexEndpointHostsCyTaskOptions } from './types'; +import type { + HostPolicyResponse, + LogsEndpointActionResponse, +} from '../../../common/endpoint/types'; +import type { IndexEndpointHostsCyTaskOptions, HostActionResponse } from './types'; import type { DeleteIndexedFleetEndpointPoliciesResponse, IndexedFleetEndpointPolicyResponse, @@ -115,6 +118,12 @@ declare global { arg: IndexedEndpointPolicyResponse, options?: Partial ): Chainable; + + task( + name: 'sendHostActionResponse', + arg: HostActionResponse, + options?: Partial + ): Chainable; } } } diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts index 35b931675a6c2f..d5c946af96c14c 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts @@ -5,18 +5,31 @@ * 2.0. */ +import type { ReturnTypeFromChainable } from '../../types'; +import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; import { login } from '../../tasks/login'; -import { runEndpointLoaderScript } from '../../tasks/run_endpoint_loader'; describe('Endpoints page', () => { + let endpointData: ReturnTypeFromChainable; + before(() => { - runEndpointLoaderScript(); + indexEndpointHosts().then((indexEndpoints) => { + endpointData = indexEndpoints; + }); }); beforeEach(() => { login(); }); + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + }); + it('Loads the endpoints page', () => { cy.visit('/app/security/administration/endpoints'); cy.contains('Hosts running Elastic Defend').should('exist'); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts new file mode 100644 index 00000000000000..579c0cab8c540a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts @@ -0,0 +1,328 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getEndpointListPath } from '../../../common/routing'; +import { + interceptActionRequests, + isolateHostWithComment, + openAlertDetails, + openCaseAlertDetails, + releaseHostWithComment, + sendActionResponse, + waitForReleaseOption, +} from '../../tasks/isolate'; +import type { ActionDetails } from '../../../../../common/endpoint/types'; +import { closeAllToasts } from '../../tasks/close_all_toasts'; +import type { ReturnTypeFromChainable } from '../../types'; +import { addAlertsToCase } from '../../tasks/add_alerts_to_case'; +import { APP_ALERTS_PATH, APP_CASES_PATH, APP_PATH } from '../../../../../common/constants'; +import { login } from '../../tasks/login'; +import { indexNewCase } from '../../tasks/index_new_case'; +import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; +import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts'; + +describe('Isolate command', () => { + describe('from Manage', () => { + let endpointData: ReturnTypeFromChainable; + let isolatedEndpointData: ReturnTypeFromChainable; + + before(() => { + indexEndpointHosts({ + count: 2, + withResponseActions: false, + isolation: false, + }).then((indexEndpoints) => { + endpointData = indexEndpoints; + }); + + indexEndpointHosts({ + count: 2, + withResponseActions: false, + isolation: true, + }).then((indexEndpoints) => { + isolatedEndpointData = indexEndpoints; + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (isolatedEndpointData) { + isolatedEndpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + isolatedEndpointData = undefined; + } + }); + beforeEach(() => { + login(); + }); + it('should allow filtering endpoint by Isolated status', () => { + cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' })); + closeAllToasts(); + cy.getByTestSubj('adminSearchBar') + .click() + .type('united.endpoint.Endpoint.state.isolation: true'); + cy.getByTestSubj('querySubmitButton').click(); + cy.contains('Showing 2 endpoints'); + cy.getByTestSubj('endpointListTable').within(() => { + cy.get('tbody tr').each(($tr) => { + cy.wrap($tr).within(() => { + cy.get('td').eq(1).should('contain.text', 'Isolated'); + }); + }); + }); + }); + }); + + describe('from Alerts', () => { + let endpointData: ReturnTypeFromChainable; + let alertData: ReturnTypeFromChainable; + let hostname: string; + + before(() => { + indexEndpointHosts({ withResponseActions: false, isolation: false }) + .then((indexEndpoints) => { + endpointData = indexEndpoints; + hostname = endpointData.data.hosts[0].host.name; + }) + .then(() => { + return indexEndpointRuleAlerts({ + endpointAgentId: endpointData.data.hosts[0].agent.id, + endpointHostname: endpointData.data.hosts[0].host.name, + endpointIsolated: false, + }); + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + // @ts-expect-error ignore setting to undefined + alertData = undefined; + } + }); + + beforeEach(() => { + login(); + }); + + it('should isolate and release host', () => { + const isolateComment = `Isolating ${hostname}`; + const releaseComment = `Releasing ${hostname}`; + let isolateRequestResponse: ActionDetails; + let releaseRequestResponse: ActionDetails; + + cy.visit(APP_ALERTS_PATH); + closeAllToasts(); + + cy.getByTestSubj('alertsTable').within(() => { + cy.getByTestSubj('expand-event') + .first() + .within(() => { + cy.get(`[data-is-loading="true"]`).should('exist'); + }); + cy.getByTestSubj('expand-event') + .first() + .within(() => { + cy.get(`[data-is-loading="true"]`).should('not.exist'); + }); + }); + + openAlertDetails(); + + isolateHostWithComment(isolateComment, hostname); + + interceptActionRequests((responseBody) => { + isolateRequestResponse = responseBody; + }, 'isolate'); + + cy.getByTestSubj('hostIsolateConfirmButton').click(); + + cy.wait('@isolate').then(() => { + sendActionResponse(isolateRequestResponse); + }); + + cy.contains(`Isolation on host ${hostname} successfully submitted`); + + cy.getByTestSubj('euiFlyoutCloseButton').click(); + cy.wait(1000); + openAlertDetails(); + cy.getByTestSubj('event-field-agent.status').then(($status) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.contains('Release host').click(); + } else { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openAlertDetails(); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.contains('Isolated'); + }); + cy.contains('Release host').click(); + } + }); + + releaseHostWithComment(releaseComment, hostname); + + interceptActionRequests((responseBody) => { + releaseRequestResponse = responseBody; + }, 'release'); + + cy.contains('Confirm').click(); + + cy.wait('@release').then(() => { + sendActionResponse(releaseRequestResponse); + }); + + cy.contains(`Release on host ${hostname} successfully submitted`); + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openAlertDetails(); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.get('[title="Isolated"]').should('not.exist'); + }); + }); + }); + + describe('from Cases', () => { + let endpointData: ReturnTypeFromChainable; + let caseData: ReturnTypeFromChainable; + let alertData: ReturnTypeFromChainable; + let caseAlertActions: ReturnType; + let alertId: string; + let caseUrlPath: string; + let hostname: string; + + before(() => { + indexNewCase().then((indexCase) => { + caseData = indexCase; + caseUrlPath = `${APP_CASES_PATH}/${indexCase.data.id}`; + }); + + indexEndpointHosts({ withResponseActions: false, isolation: false }) + .then((indexEndpoints) => { + endpointData = indexEndpoints; + hostname = endpointData.data.hosts[0].host.name; + }) + .then(() => { + return indexEndpointRuleAlerts({ + endpointAgentId: endpointData.data.hosts[0].agent.id, + endpointHostname: endpointData.data.hosts[0].host.name, + endpointIsolated: false, + }).then((indexedAlert) => { + alertData = indexedAlert; + alertId = alertData.alerts[0]._id; + }); + }) + .then(() => { + caseAlertActions = addAlertsToCase({ + caseId: caseData.data.id, + alertIds: [alertId], + }); + }); + }); + + after(() => { + if (caseData) { + caseData.cleanup(); + // @ts-expect-error ignore setting to undefined + caseData = undefined; + } + + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + // @ts-expect-error ignore setting to undefined + alertData = undefined; + } + }); + + beforeEach(() => { + login(); + }); + + it('should isolate and release host', () => { + let isolateRequestResponse: ActionDetails; + let releaseRequestResponse: ActionDetails; + const isolateComment = `Isolating ${hostname}`; + const releaseComment = `Releasing ${hostname}`; + const caseAlertId = caseAlertActions.comments[alertId]; + + cy.visit(caseUrlPath); + closeAllToasts(); + openCaseAlertDetails(caseAlertId); + + isolateHostWithComment(isolateComment, hostname); + + interceptActionRequests((responseBody) => { + isolateRequestResponse = responseBody; + }, 'isolate'); + + cy.getByTestSubj('hostIsolateConfirmButton').click(); + + cy.wait('@isolate').then(() => { + sendActionResponse(isolateRequestResponse); + }); + + cy.contains(`Isolation on host ${hostname} successfully submitted`); + + cy.getByTestSubj('euiFlyoutCloseButton').click(); + + cy.getByTestSubj('user-actions-list').within(() => { + cy.contains(isolateComment); + cy.get('[aria-label="lock"]').should('exist'); + cy.get('[aria-label="lockOpen"]').should('not.exist'); + }); + + waitForReleaseOption(caseAlertId); + + releaseHostWithComment(releaseComment, hostname); + + interceptActionRequests((responseBody) => { + releaseRequestResponse = responseBody; + }, 'release'); + + cy.contains('Confirm').click(); + + cy.wait('@release').then(() => { + sendActionResponse(releaseRequestResponse); + }); + + cy.contains(`Release on host ${hostname} successfully submitted`); + cy.getByTestSubj('euiFlyoutCloseButton').click(); + + cy.getByTestSubj('user-actions-list').within(() => { + cy.contains(releaseComment); + cy.contains(isolateComment); + cy.get('[aria-label="lock"]').should('exist'); + cy.get('[aria-label="lockOpen"]').should('exist'); + }); + + openCaseAlertDetails(caseAlertId); + cy.getByTestSubj('event-field-agent.status').then(($status) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + cy.getByTestSubj(`comment-action-show-alert-${caseAlertId}`).click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); + } + cy.get('[title="Isolated"]').should('not.exist'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index b31ee2ec258746..6dd4bedaa89378 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -8,12 +8,17 @@ // / import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; +import { sendEndpointActionResponse } from '../../../../scripts/endpoint/agent_emulator/services/endpoint_response_actions'; import type { IndexedEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response'; import { deleteIndexedEndpointPolicyResponse, indexEndpointPolicyResponse, } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response'; -import type { HostPolicyResponse } from '../../../../common/endpoint/types'; +import type { + ActionDetails, + HostPolicyResponse, + LogsEndpointActionResponse, +} from '../../../../common/endpoint/types'; import type { IndexEndpointHostsCyTaskOptions } from '../types'; import type { IndexedEndpointRuleAlerts, @@ -95,12 +100,14 @@ export const dataLoaders = ( indexEndpointHosts: async (options: IndexEndpointHostsCyTaskOptions = {}) => { const { kbnClient, esClient } = await stackServicesPromise; - const { count: numHosts, version, os } = options; + const { count: numHosts, version, os, isolation, withResponseActions } = options; return cyLoadEndpointDataHandler(esClient, kbnClient, { numHosts, version, os, + isolation, + withResponseActions, }); }, @@ -140,5 +147,13 @@ export const dataLoaders = ( const { esClient } = await stackServicesPromise; return deleteIndexedEndpointPolicyResponse(esClient, indexedData).then(() => null); }, + + sendHostActionResponse: async (data: { + action: ActionDetails; + state: { state?: 'success' | 'failure' }; + }): Promise => { + const { esClient } = await stackServicesPromise; + return sendEndpointActionResponse(esClient, data.action, { state: data.state.state }); + }, }); }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts index cfff2e9a2a4b01..9d0f5ac135d5d1 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts @@ -36,6 +36,9 @@ export interface CyLoadEndpointDataOptions enableFleetIntegration: boolean; generatorSeed: string; waitUntilTransformed: boolean; + withResponseActions: boolean; + isolation: boolean; + bothIsolatedAndNormalEndpoints?: boolean; } /** @@ -58,10 +61,12 @@ export const cyLoadEndpointDataHandler = async ( waitUntilTransformed = true, version = kibanaPackageJson.version, os, + withResponseActions, + isolation, } = options; const DocGenerator = EndpointDocGenerator.custom({ - CustomMetadataGenerator: EndpointMetadataGenerator.custom({ version, os }), + CustomMetadataGenerator: EndpointMetadataGenerator.custom({ version, os, isolation }), }); if (waitUntilTransformed) { @@ -85,7 +90,8 @@ export const cyLoadEndpointDataHandler = async ( alertsPerHost, enableFleetIntegration, undefined, - DocGenerator + DocGenerator, + withResponseActions ); if (waitUntilTransformed) { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts index 498c105d0da497..21ebd75fa63293 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts @@ -12,6 +12,8 @@ import type { export const indexEndpointRuleAlerts = (options: { endpointAgentId: string; + endpointHostname?: string; + endpointIsolated?: boolean; count?: number; }): Cypress.Chainable< Pick & { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts new file mode 100644 index 00000000000000..b00b567852026c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ActionDetails } from '../../../../common/endpoint/types'; + +const API_ENDPOINT_ACTION_PATH = '/api/endpoint/action/*'; +export const interceptActionRequests = ( + cb: (responseBody: ActionDetails) => void, + alias: string +): void => { + cy.intercept('POST', API_ENDPOINT_ACTION_PATH, (req) => { + req.continue((res) => { + const { + body: { action, data }, + } = res; + + cb({ action, ...data }); + }); + }).as(alias); +}; + +export const sendActionResponse = (action: ActionDetails): void => { + cy.task('sendHostActionResponse', { + action, + state: { state: 'success' }, + }); +}; + +export const isolateHostWithComment = (comment: string, hostname: string): void => { + cy.getByTestSubj('isolate-host-action-item').click(); + cy.contains(`Isolate host ${hostname} from network.`); + cy.getByTestSubj('endpointHostIsolationForm'); + cy.getByTestSubj('host_isolation_comment').type(comment); +}; + +export const releaseHostWithComment = (comment: string, hostname: string): void => { + cy.contains(`${hostname} is currently isolated.`); + cy.getByTestSubj('endpointHostIsolationForm'); + cy.getByTestSubj('host_isolation_comment').type(comment); +}; + +export const openAlertDetails = (): void => { + cy.getByTestSubj('expand-event').first().click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); +}; + +export const openCaseAlertDetails = (alertId: string): void => { + cy.getByTestSubj(`comment-action-show-alert-${alertId}`).click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); +}; +export const waitForReleaseOption = (alertId: string): void => { + openCaseAlertDetails(alertId); + cy.getByTestSubj('event-field-agent.status').then(($status) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.contains('Release host').click(); + } else { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openCaseAlertDetails(alertId); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.contains('Isolated'); + }); + cy.contains('Release host').click(); + } + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/types.ts b/x-pack/plugins/security_solution/public/management/cypress/types.ts index 0741f7fab1ad02..97d635a3b68408 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/types.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/types.ts @@ -7,6 +7,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { ActionDetails } from '../../../common/endpoint/types'; import type { CyLoadEndpointDataOptions } from './support/plugin_handlers/endpoint_data_loader'; type PossibleChainable = @@ -41,5 +42,15 @@ export type ReturnTypeFromChainable = C extends Cyp : never; export type IndexEndpointHostsCyTaskOptions = Partial< - { count: number } & Pick + { count: number; withResponseActions: boolean } & Pick< + CyLoadEndpointDataOptions, + 'version' | 'os' | 'isolation' + > >; + +export interface HostActionResponse { + data: { + action: ActionDetails; + state: { state?: 'success' | 'failure' }; + }; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index b72d4fa30777d3..40abffc508fabf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -256,15 +256,15 @@ describe('endpoint list middleware', () => { query: { agent_ids: [ '0dc3661d-6e67-46b0-af39-6f12b025fcb0', - '34634c58-24b4-4448-80f4-107fb9918494', - '5a1298e3-e607-4bc0-8ef6-6d6a811312f2', - '78c54b13-596d-4891-95f4-80092d04454b', - '445f1fd2-5f81-4ddd-bdb6-f0d1bf2efe90', - 'd77a3fc6-3096-4852-a6ee-f6b09278fbc6', - '892fcccf-1bd8-45a2-a9cc-9a7860a3cb81', - '693a3110-5ba0-4284-a264-5d78301db08c', - '554db084-64fa-4e4a-ba47-2ba713f9932b', - 'c217deb6-674d-4f97-bb1d-a3a04238e6d7', + 'fe16dda9-7f34-434c-9824-b4844880f410', + 'f412728b-929c-48d5-bdb6-5a1298e3e607', + 'd0405ddc-1e7c-48f0-93d7-d55f954bd745', + '46d78dd2-aedf-4d3f-b3a9-da445f1fd25f', + '5aafa558-26b8-4bb4-80e2-ac0644d77a3f', + 'edac2c58-1748-40c3-853c-8fab48c333d7', + '06b7223a-bb2a-428a-9021-f1c0d2267ada', + 'b8daa43b-7f73-4684-9221-dbc8b769405e', + 'fbc06310-7d41-46b8-a5ea-ceed8a993b1a', ], }, }); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/endpoint_response_actions.ts b/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/endpoint_response_actions.ts index 2b3d73efac3617..25c2e5f6327be5 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/endpoint_response_actions.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/agent_emulator/services/endpoint_response_actions.ts @@ -137,15 +137,21 @@ export const sendEndpointActionResponse = async ( message: 'Endpoint encountered an error and was unable to apply action to host', }; - if (endpointResponse.EndpointActions.data.command === 'get-file') { + if ( + endpointResponse.EndpointActions.data.command === 'get-file' && + endpointResponse.EndpointActions.data.output + ) { ( - endpointResponse.EndpointActions.data.output?.content as ResponseActionGetFileOutputContent + endpointResponse.EndpointActions.data.output.content as ResponseActionGetFileOutputContent ).code = endpointActionGenerator.randomGetFileFailureCode(); } - if (endpointResponse.EndpointActions.data.command === 'execute') { + if ( + endpointResponse.EndpointActions.data.command === 'execute' && + endpointResponse.EndpointActions.data.output + ) { ( - endpointResponse.EndpointActions.data.output?.content as ResponseActionExecuteOutputContent + endpointResponse.EndpointActions.data.output.content as ResponseActionExecuteOutputContent ).stderr = 'execute command timed out'; } } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 5d199320901680..735285753ea235 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -33,27 +33,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Actions', ], [ - 'Host-dpu1a2r2yi', + 'Host-9qenwrl9ko', 'x', 'x', 'Warning', - 'macOS', - '10.2.17.24, 10.56.215.200,10.254.196.130', - 'x', - 'x', - '', - ], - [ - 'Host-rs9wp4o6l9', - 'x', - 'x', - 'Success', 'Linux', - '10.138.79.131, 10.170.160.154', + '10.56.228.101, 10.201.120.140,10.236.180.146', 'x', 'x', '', ], + ['Host-qw2bti801m', 'x', 'x', 'Failure', 'macOS', '10.244.59.227', 'x', 'x', ''], [ 'Host-u5jy6j0pwb', 'x', From fe75fbd55d2dc6e77435c66561ba381238dcb9e0 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 24 Apr 2023 08:14:48 -0400 Subject: [PATCH 39/65] [Cases] Rounding the file size to avoid decimals (#155542) This PR fixes a bug in the telemetry code around the average file size. A float can be returned by the average aggregation. To avoid the float we'll round it before saving the value in the document. --- .../server/telemetry/queries/utils.test.ts | 82 +++++++++++++++++++ .../cases/server/telemetry/queries/utils.ts | 10 ++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts index 359aa621798f00..50b5b142fd319c 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts @@ -565,6 +565,88 @@ describe('utils', () => { }); describe('files', () => { + it('rounds the average file size when it is a decimal', () => { + const attachmentFramework: AttachmentFrameworkAggsResult = { + externalReferenceTypes: { + buckets: [ + { + doc_count: 5, + key: '.files', + references: { + cases: { + max: { + value: 10, + }, + }, + }, + }, + ], + }, + persistableReferenceTypes: { + buckets: [], + }, + }; + + expect( + getAttachmentsFrameworkStats({ + attachmentAggregations: attachmentFramework, + totalCasesForOwner: 5, + filesAggregations: { + averageSize: { value: 1.1 }, + topMimeTypes: { + buckets: [], + }, + }, + }).attachmentFramework.files + ).toMatchInlineSnapshot(` + Object { + "average": 1, + "averageSize": 1, + "maxOnACase": 10, + "topMimeTypes": Array [], + "total": 5, + } + `); + }); + + it('sets the average file size to 0 when the aggregation does not exist', () => { + const attachmentFramework: AttachmentFrameworkAggsResult = { + externalReferenceTypes: { + buckets: [ + { + doc_count: 5, + key: '.files', + references: { + cases: { + max: { + value: 10, + }, + }, + }, + }, + ], + }, + persistableReferenceTypes: { + buckets: [], + }, + }; + + expect( + getAttachmentsFrameworkStats({ + attachmentAggregations: attachmentFramework, + totalCasesForOwner: 5, + }).attachmentFramework.files + ).toMatchInlineSnapshot(` + Object { + "average": 1, + "averageSize": 0, + "maxOnACase": 10, + "topMimeTypes": Array [], + "total": 5, + } + `); + }); + it('sets the files stats to empty when the file aggregation results is the empty version', () => { const attachmentFramework: AttachmentFrameworkAggsResult = { externalReferenceTypes: { diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts index 7896c2bdac7609..e47e9d1613bce7 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts @@ -230,7 +230,7 @@ export const getAttachmentsFrameworkStats = ({ return emptyAttachmentFramework(); } - const averageFileSize = filesAggregations?.averageSize?.value; + const averageFileSize = getAverageFileSize(filesAggregations); const topMimeTypes = filesAggregations?.topMimeTypes; return { @@ -253,6 +253,14 @@ export const getAttachmentsFrameworkStats = ({ }; }; +const getAverageFileSize = (filesAggregations?: FileAttachmentAggsResult) => { + if (filesAggregations?.averageSize?.value == null) { + return 0; + } + + return Math.round(filesAggregations.averageSize.value); +}; + const getAttachmentRegistryStats = ( registryResults: BucketsWithMaxOnCase, totalCasesForOwner: number From 755ddfe9cdfbf8df05f404b66997d0629ec3db97 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Mon, 24 Apr 2023 14:18:52 +0200 Subject: [PATCH 40/65] [Enterprise Search] Copyedit Elasticsearch, Search Applications (#155604) Minor copy clean up for clarity and concision --- .../components/elasticsearch_guide/elasticsearch_guide.tsx | 4 ++-- .../components/engines/engines_list.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx index 3820f8e334f07c..7da4392ef1112d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx @@ -79,7 +79,7 @@ export const ElasticsearchGuide: React.FC = () => { 'xpack.enterpriseSearch.overview.elasticsearchGuide.elasticsearchDescription', { defaultMessage: - 'Whether you are building a search-powered application, or designing a large-scale search implementation, Elasticsearch provides the low-level tools to create the most relevant and performant search experience.', + "Elasticsearch provides the low-level tools you need to build fast, relevant search for your website or application. Because it's powerful and flexible, Elasticsearch can handle search use cases of all shapes and sizes.", } )}

@@ -103,7 +103,7 @@ export const ElasticsearchGuide: React.FC = () => { 'xpack.enterpriseSearch.overview.elasticsearchGuide.connectToElasticsearchDescription', { defaultMessage: - "Elastic builds and maintains clients in several popular languages and our community has contributed many more. They're easy to work with, feel natural to use, and, just like Elasticsearch, don't limit what you might want to do with them.", + 'Elastic builds and maintains clients in several popular languages and our community has contributed many more.', } )}

diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx index 48dce9f524164e..7b93214e0af8bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx @@ -166,7 +166,7 @@ export const EnginesList: React.FC = ({ createEngineFlyoutOpen }) => description: ( Date: Mon, 24 Apr 2023 08:20:44 -0400 Subject: [PATCH 41/65] [Synthetics] Improve toast information for add/update monitor (#155319) --- ...e_monitor_save.ts => use_monitor_save.tsx} | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) rename x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/{use_monitor_save.ts => use_monitor_save.tsx} (74%) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx similarity index 74% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx index 9fd18e5dfa04d4..d8638c4b9ed926 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx @@ -6,8 +6,9 @@ */ import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public'; +import { toMountPoint, useKibana } from '@kbn/kibana-react-plugin/public'; import { useParams, useRouteMatch } from 'react-router-dom'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { MONITOR_EDIT_ROUTE } from '../../../../../../common/constants'; @@ -18,6 +19,8 @@ import { cleanMonitorListState } from '../../../state'; import { useSyntheticsRefreshContext } from '../../../contexts'; export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonitor }) => { + const core = useKibana(); + const theme$ = core.services.theme?.theme$; const dispatch = useDispatch(); const { refreshApp } = useSyntheticsRefreshContext(); const { monitorId } = useParams<{ monitorId: string }>(); @@ -51,10 +54,16 @@ export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonito dispatch(cleanMonitorListState()); kibanaService.toasts.addSuccess({ title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, + text: toMountPoint( +

+ {monitorId ? MONITOR_UPDATED_SUCCESS_LABEL_SUBTEXT : MONITOR_SUCCESS_LABEL_SUBTEXT} +

, + { theme$ } + ), toastLifeTimeMs: 3000, }); } - }, [data, status, monitorId, loading, refreshApp, dispatch]); + }, [data, status, monitorId, loading, refreshApp, dispatch, theme$]); return { status, loading, isEdit }; }; @@ -66,6 +75,13 @@ const MONITOR_SUCCESS_LABEL = i18n.translate( } ); +const MONITOR_SUCCESS_LABEL_SUBTEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage.subtext', + { + defaultMessage: 'It will next run according to its defined schedule.', + } +); + const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( 'xpack.synthetics.monitorManagement.monitorEditedSuccessMessage', { @@ -79,3 +95,10 @@ const MONITOR_FAILURE_LABEL = i18n.translate( defaultMessage: 'Monitor was unable to be saved. Please try again later.', } ); + +const MONITOR_UPDATED_SUCCESS_LABEL_SUBTEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorFailureMessage.subtext', + { + defaultMessage: 'It will next run according to its defined schedule.', + } +); From 9eee24f7bfc557d9aff48e30f3358d542ef5f476 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 24 Apr 2023 06:01:05 -0700 Subject: [PATCH 42/65] [Security Solution] Multi level grouping for alerts table (#152862) ## Multi Level Grouping Resolves https://github.com/elastic/kibana/issues/150516 Resolves https://github.com/elastic/kibana/issues/150514 Implements multi level grouping in Alerts table and Rule details table. Supports 3 levels deep. https://user-images.githubusercontent.com/6935300/232547389-7d778f69-d96d-4bd8-8560-f5ddd9fe8060.mov ### Test plan https://docs.google.com/document/d/15oseanNzF-u-Xeoahy1IVxI4oV3wOuO8VhA886cA1U8/edit# ### To do - [Cypress](https://github.com/elastic/kibana/issues/150666) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Steph Milovic --- .../steps/storybooks/build_and_upload.ts | 2 + .../.storybook/main.js | 9 + .../kbn-securitysolution-grouping/README.md | 3 - .../kbn-securitysolution-grouping/README.mdx | 3 + .../kbn-securitysolution-grouping/index.tsx | 8 +- .../accordion_panel/group_stats.test.tsx | 4 +- .../accordion_panel/group_stats.tsx | 31 +- .../components/accordion_panel/index.test.tsx | 1 + .../src/components/accordion_panel/index.tsx | 30 +- .../components/group_selector/index.test.tsx | 36 +- .../src/components/group_selector/index.tsx | 61 +- .../src/components/grouping.mock.tsx | 111 ++ .../src/components/grouping.stories.tsx | 28 + .../src/components/grouping.test.tsx | 95 +- .../src/components/grouping.tsx | 159 +- .../src/components/index.tsx | 5 +- .../src/components/styles.tsx | 37 +- .../src/components/translations.ts | 8 +- .../src/components/types.ts | 6 +- .../src/containers/query/index.ts | 2 +- .../src/containers/query/types.ts | 3 +- .../src/hooks/state/actions.ts | 53 +- .../src/hooks/state/reducer.test.ts | 39 +- .../src/hooks/state/reducer.ts | 48 +- .../src/hooks/types.ts | 32 +- .../src/hooks/use_get_group_selector.test.tsx | 115 +- .../src/hooks/use_get_group_selector.tsx | 116 +- .../src/hooks/use_grouping.test.tsx | 13 +- .../src/hooks/use_grouping.tsx | 80 +- .../src/hooks/use_grouping_pagination.ts | 53 - .../tsconfig.json | 3 +- src/dev/storybook/aliases.ts | 1 + .../alerts_treemap_panel/index.test.tsx | 11 - .../components/alerts_treemap_panel/index.tsx | 2 +- .../public/common/components/top_n/index.tsx | 2 +- .../alerts/use_alert_prevalence.test.ts | 46 - .../containers/alerts/use_alert_prevalence.ts | 2 +- .../containers/use_global_time/index.test.tsx | 74 +- .../containers/use_global_time/index.tsx | 22 +- .../public/common/store/grouping/actions.ts | 6 +- .../public/common/store/grouping/reducer.ts | 15 +- .../public/common/store/grouping/selectors.ts | 4 - .../public/common/store/grouping/types.ts | 1 - .../pages/rule_details/index.tsx | 2 +- .../alerts_count_panel/index.test.tsx | 13 - .../alerts_kpis/alerts_count_panel/index.tsx | 2 +- .../alerts_histogram_panel/index.tsx | 2 +- .../use_summary_chart_data.tsx | 2 +- .../alerts_table/alerts_grouping.test.tsx | 376 ++++ .../alerts_table/alerts_grouping.tsx | 341 ++-- .../alerts_table/alerts_sub_grouping.tsx | 259 +++ .../group_take_action_items.test.tsx | 119 +- .../group_take_action_items.tsx | 147 +- .../alerts_table/grouping_settings/mock.ts | 1736 +++++++++++++++++ .../grouping_settings/query_builder.ts | 6 +- .../components/alerts_table/index.test.tsx | 261 --- .../use_persistent_controls.tsx | 8 +- .../detection_engine.test.tsx | 159 +- .../detection_engine/detection_engine.tsx | 30 +- .../entity_analytics/anomalies/index.tsx | 2 +- .../entity_analytics/header/index.tsx | 2 +- 61 files changed, 3631 insertions(+), 1216 deletions(-) create mode 100644 packages/kbn-securitysolution-grouping/.storybook/main.js delete mode 100644 packages/kbn-securitysolution-grouping/README.md create mode 100644 packages/kbn-securitysolution-grouping/README.mdx create mode 100644 packages/kbn-securitysolution-grouping/src/components/grouping.mock.tsx create mode 100644 packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx delete mode 100644 packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts delete mode 100644 x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index 949cb0a0ff5349..b16e75abdb8a16 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -15,6 +15,7 @@ const STORYBOOKS = [ 'apm', 'canvas', 'cases', + 'cell_actions', 'ci_composite', 'cloud_chat', 'coloring', @@ -34,6 +35,7 @@ const STORYBOOKS = [ 'expression_shape', 'expression_tagcloud', 'fleet', + 'grouping', 'home', 'infra', 'kibana_react', diff --git a/packages/kbn-securitysolution-grouping/.storybook/main.js b/packages/kbn-securitysolution-grouping/.storybook/main.js new file mode 100644 index 00000000000000..8dc3c5d1518f4d --- /dev/null +++ b/packages/kbn-securitysolution-grouping/.storybook/main.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/packages/kbn-securitysolution-grouping/README.md b/packages/kbn-securitysolution-grouping/README.md deleted file mode 100644 index 87b8047720a378..00000000000000 --- a/packages/kbn-securitysolution-grouping/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/securitysolution-grouping - -Grouping component and query. Currently only consumed by security solution alerts table. Package is a WIP. Refactoring to make generic https://github.com/elastic/kibana/issues/152491 diff --git a/packages/kbn-securitysolution-grouping/README.mdx b/packages/kbn-securitysolution-grouping/README.mdx new file mode 100644 index 00000000000000..b79cac381c2987 --- /dev/null +++ b/packages/kbn-securitysolution-grouping/README.mdx @@ -0,0 +1,3 @@ +# @kbn/securitysolution-grouping + +Grouping component and query. Currently only consumed by security solution alerts table. diff --git a/packages/kbn-securitysolution-grouping/index.tsx b/packages/kbn-securitysolution-grouping/index.tsx index 92d69af316e1fd..1b83c314714b7f 100644 --- a/packages/kbn-securitysolution-grouping/index.tsx +++ b/packages/kbn-securitysolution-grouping/index.tsx @@ -6,20 +6,22 @@ * Side Public License, v 1. */ -import { RawBucket, StatRenderer, getGroupingQuery, isNoneGroup, useGrouping } from './src'; +import { getGroupingQuery, isNoneGroup, useGrouping } from './src'; import type { + DynamicGroupingProps, GroupOption, GroupingAggregation, - GroupingFieldTotalAggregation, NamedAggregation, + RawBucket, + StatRenderer, } from './src'; export { getGroupingQuery, isNoneGroup, useGrouping }; export type { + DynamicGroupingProps, GroupOption, GroupingAggregation, - GroupingFieldTotalAggregation, NamedAggregation, RawBucket, StatRenderer, diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx index 8df4c6ad6c7dce..8ccc1c912b62de 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx @@ -13,6 +13,8 @@ import { GroupStats } from './group_stats'; const onTakeActionsOpen = jest.fn(); const testProps = { bucketKey: '9nk5mo2fby', + groupFilter: [], + groupNumber: 0, onTakeActionsOpen, statRenderers: [ { @@ -23,7 +25,7 @@ const testProps = { { title: 'Rules:', badge: { value: 2 } }, { title: 'Alerts:', badge: { value: 2, width: 50, color: '#a83632' } }, ], - takeActionItems: [ + takeActionItems: () => [

,

, ], diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx index 00c6e7aa3a855d..61f40982507b85 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx @@ -16,29 +16,44 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { Filter } from '@kbn/es-query'; import { StatRenderer } from '../types'; import { statsContainerCss } from '../styles'; import { TAKE_ACTION } from '../translations'; interface GroupStatsProps { bucketKey: string; - statRenderers?: StatRenderer[]; + groupFilter: Filter[]; + groupNumber: number; onTakeActionsOpen?: () => void; - takeActionItems: JSX.Element[]; + statRenderers?: StatRenderer[]; + takeActionItems: (groupFilters: Filter[], groupNumber: number) => JSX.Element[]; } const GroupStatsComponent = ({ bucketKey, - statRenderers, + groupFilter, + groupNumber, onTakeActionsOpen, - takeActionItems, + statRenderers, + takeActionItems: getTakeActionItems, }: GroupStatsProps) => { const [isPopoverOpen, setPopover] = useState(false); + const [takeActionItems, setTakeActionItems] = useState([]); - const onButtonClick = useCallback( - () => (!isPopoverOpen && onTakeActionsOpen ? onTakeActionsOpen() : setPopover(!isPopoverOpen)), - [isPopoverOpen, onTakeActionsOpen] - ); + const onButtonClick = useCallback(() => { + if (!isPopoverOpen && takeActionItems.length === 0) { + setTakeActionItems(getTakeActionItems(groupFilter, groupNumber)); + } + return !isPopoverOpen && onTakeActionsOpen ? onTakeActionsOpen() : setPopover(!isPopoverOpen); + }, [ + getTakeActionItems, + groupFilter, + groupNumber, + isPopoverOpen, + onTakeActionsOpen, + takeActionItems.length, + ]); const statsComponent = useMemo( () => diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx index 9aa4b514371308..828e1059471e91 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx @@ -55,6 +55,7 @@ const testProps = { }, renderChildComponent, selectedGroup: 'kibana.alert.rule.name', + onGroupClose: () => {}, }; describe('grouping accordion panel', () => { diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx index 286cb18ffb6e69..c1d55495cf785c 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx @@ -8,7 +8,7 @@ import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { firstNonNullValue } from '../../helpers'; import type { RawBucket } from '../types'; import { createGroupFilter } from './helpers'; @@ -20,8 +20,9 @@ interface GroupPanelProps { forceState?: 'open' | 'closed'; groupBucket: RawBucket; groupPanelRenderer?: JSX.Element; + groupingLevel?: number; isLoading: boolean; - level?: number; + onGroupClose: () => void; onToggleGroup?: (isOpen: boolean, groupBucket: RawBucket) => void; renderChildComponent: (groupFilter: Filter[]) => React.ReactElement; selectedGroup: string; @@ -40,18 +41,30 @@ const DefaultGroupPanelRenderer = ({ title }: { title: string }) => ( ); const GroupPanelComponent = ({ - customAccordionButtonClassName = 'groupingAccordionForm__button', + customAccordionButtonClassName, customAccordionClassName = 'groupingAccordionForm', extraAction, forceState, groupBucket, groupPanelRenderer, + groupingLevel = 0, isLoading, - level = 0, + onGroupClose, onToggleGroup, renderChildComponent, selectedGroup, }: GroupPanelProps) => { + const lastForceState = useRef(forceState); + useEffect(() => { + if (lastForceState.current === 'open' && forceState === 'closed') { + // when parent group closes, reset pagination of any child groups + onGroupClose(); + lastForceState.current = 'closed'; + } else if (lastForceState.current === 'closed' && forceState === 'open') { + lastForceState.current = 'open'; + } + }, [onGroupClose, forceState, selectedGroup]); + const groupFieldValue = useMemo(() => firstNonNullValue(groupBucket.key), [groupBucket.key]); const groupFilters = useMemo( @@ -72,20 +85,21 @@ const GroupPanelComponent = ({ +

{groupPanelRenderer ?? }
} - className={customAccordionClassName} + buttonElement="div" + className={groupingLevel > 0 ? 'groupingAccordionFormLevel' : customAccordionClassName} data-test-subj="grouping-accordion" extraAction={extraAction} forceState={forceState} isLoading={isLoading} - id={`group${level}-${groupFieldValue}`} + id={`group${groupingLevel}-${groupFieldValue}`} onToggle={onToggle} paddingSize="m" > - {renderChildComponent(groupFilters)} + {renderChildComponent(groupFilters)} ); }; diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx index daa58396df70b6..2172390f41e855 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx @@ -43,7 +43,7 @@ const testProps = { esTypes: ['ip'], }, ], - groupSelected: 'kibana.alert.rule.name', + groupsSelected: ['kibana.alert.rule.name'], onGroupChange, options: [ { @@ -90,4 +90,38 @@ describe('group selector', () => { fireEvent.click(getByTestId('panel-none')); expect(onGroupChange).toHaveBeenCalled(); }); + it('Labels button in correct selection order', () => { + const { getByTestId, rerender } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, User name, Host name'); + rerender( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name, User name'); + }); + it('Labels button with selection not in options', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name'); + }); + it('Labels button when `none` is selected', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name'); + }); }); diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx index f0274f7c73ab71..a2a876da979921 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx @@ -21,21 +21,27 @@ export interface GroupSelectorProps { 'data-test-subj'?: string; fields: FieldSpec[]; groupingId: string; - groupSelected: string; + groupsSelected: string[]; onGroupChange: (groupSelection: string) => void; options: Array<{ key: string; label: string }>; title?: string; + maxGroupingLevels?: number; } - const GroupSelectorComponent = ({ 'data-test-subj': dataTestSubj, fields, - groupSelected = 'none', + groupsSelected = ['none'], onGroupChange, options, title = i18n.GROUP_BY, + maxGroupingLevels = 1, }: GroupSelectorProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const isGroupSelected = useCallback( + (groupKey: string) => + !!groupsSelected.find((selectedGroupKey) => selectedGroupKey === groupKey), + [groupsSelected] + ); const panels: EuiContextMenuPanelDescriptor[] = useMemo( () => [ @@ -49,7 +55,7 @@ const GroupSelectorComponent = ({ style={{ lineHeight: 1 }} > - {i18n.SELECT_FIELD.toUpperCase()} + {i18n.SELECT_FIELD(maxGroupingLevels)} onGroupChange('none'), }, ...options.map((o) => ({ 'data-test-subj': `panel-${o.key}`, + disabled: groupsSelected.length === maxGroupingLevels && !isGroupSelected(o.key), name: o.label, onClick: () => onGroupChange(o.key), - icon: groupSelected === o.key ? 'check' : 'empty', + icon: isGroupSelected(o.key) ? 'check' : 'empty', })), { 'data-test-subj': `panel-custom`, name: i18n.CUSTOM_FIELD, icon: 'empty', + disabled: groupsSelected.length === maxGroupingLevels, panel: 'customPanel', + hasPanel: true, }, ], }, @@ -91,24 +100,35 @@ const GroupSelectorComponent = ({ currentOptions={options.map((o) => ({ text: o.label, field: o.key }))} onSubmit={(field: string) => { onGroupChange(field); + setIsPopoverOpen(false); }} fields={fields} /> ), }, ], - [fields, groupSelected, onGroupChange, options] + [fields, groupsSelected.length, isGroupSelected, maxGroupingLevels, onGroupChange, options] ); - const selectedOption = useMemo( - () => options.filter((groupOption) => groupOption.key === groupSelected), - [groupSelected, options] + const selectedOptions = useMemo( + () => options.filter((groupOption) => isGroupSelected(groupOption.key)), + [isGroupSelected, options] ); const onButtonClick = useCallback(() => setIsPopoverOpen((currentVal) => !currentVal), []); const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const button = useMemo( - () => ( + const button = useMemo(() => { + // need to use groupsSelected to ensure proper selection order (selectedOptions does not handle selection order) + const buttonLabel = isGroupSelected('none') + ? i18n.NONE + : groupsSelected.reduce((optionsTitle, o) => { + const selection = selectedOptions.find((opt) => opt.key === o); + if (selection == null) { + return optionsTitle; + } + return optionsTitle ? [optionsTitle, selection.label].join(', ') : selection.label; + }, ''); + return ( 0 - ? selectedOption[0].label - : i18n.NONE - } + title={buttonLabel} size="xs" > - {`${title}: ${ - groupSelected !== 'none' && selectedOption.length > 0 - ? selectedOption[0].label - : i18n.NONE - }`} + {`${title}: ${buttonLabel}`} - ), - [groupSelected, onButtonClick, selectedOption, title] - ); + ); + }, [groupsSelected, isGroupSelected, onButtonClick, selectedOptions, title]); return (

{'child component'}

, + onGroupClose: () => {}, + selectedGroup: 'kibana.alert.rule.name', + takeActionItems: () => [ + {}}> + {'Mark as acknowledged'} + , + {}}> + {'Mark as closed'} + , + ], + tracker: () => {}, +}; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx new file mode 100644 index 00000000000000..b961402ee3a7cb --- /dev/null +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { mockGroupingProps } from './grouping.mock'; +import { Grouping } from './grouping'; +import readme from '../../README.mdx'; + +export default { + component: Grouping, + title: 'Grouping', + description: 'A group of accordion components that each renders a given child component', + parameters: { + docs: { + page: readme, + }, + }, +}; + +export const Emtpy: Story = () => { + return ; +}; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx index ff5a7d66a6042b..2376614ab444c5 100644 --- a/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx @@ -14,104 +14,15 @@ import { createGroupFilter } from './accordion_panel/helpers'; import { METRIC_TYPE } from '@kbn/analytics'; import { getTelemetryEvent } from '../telemetry/const'; +import { mockGroupingProps, rule1Name, rule2Name } from './grouping.mock'; + const renderChildComponent = jest.fn(); const takeActionItems = jest.fn(); const mockTracker = jest.fn(); -const rule1Name = 'Rule 1 name'; -const rule1Desc = 'Rule 1 description'; -const rule2Name = 'Rule 2 name'; -const rule2Desc = 'Rule 2 description'; const testProps = { - data: { - groupsCount: { - value: 2, - }, - groupByFields: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: [rule1Name, rule1Desc], - key_as_string: `${rule1Name}|${rule1Desc}`, - doc_count: 1, - hostsCountAggregation: { - value: 1, - }, - ruleTags: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - alertsCount: { - value: 1, - }, - severitiesSubAggregation: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'low', - doc_count: 1, - }, - ], - }, - countSeveritySubAggregation: { - value: 1, - }, - usersCountAggregation: { - value: 1, - }, - }, - { - key: [rule2Name, rule2Desc], - key_as_string: `${rule2Name}|${rule2Desc}`, - doc_count: 1, - hostsCountAggregation: { - value: 1, - }, - ruleTags: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - unitsCount: { - value: 1, - }, - severitiesSubAggregation: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'low', - doc_count: 1, - }, - ], - }, - countSeveritySubAggregation: { - value: 1, - }, - usersCountAggregation: { - value: 1, - }, - }, - ], - }, - unitsCount: { - value: 2, - }, - }, - groupingId: 'test-grouping-id', - isLoading: false, - pagination: { - pageIndex: 0, - pageSize: 25, - onChangeItemsPerPage: jest.fn(), - onChangePage: jest.fn(), - itemsPerPageOptions: [10, 25, 50, 100], - }, + ...mockGroupingProps, renderChildComponent, - selectedGroup: 'kibana.alert.rule.name', takeActionItems, tracker: mockTracker, }; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx index d77f1fe1a81061..625beda320d049 100644 --- a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx @@ -21,35 +21,29 @@ import { createGroupFilter } from './accordion_panel/helpers'; import { GroupPanel } from './accordion_panel'; import { GroupStats } from './accordion_panel/group_stats'; import { EmptyGroupingComponent } from './empty_results_panel'; -import { groupingContainerCss, countCss } from './styles'; +import { countCss, groupingContainerCss, groupingContainerCssLevel } from './styles'; import { GROUPS_UNIT } from './translations'; -import type { - GroupingAggregation, - GroupingFieldTotalAggregation, - GroupPanelRenderer, - RawBucket, -} from './types'; -import { getTelemetryEvent } from '../telemetry/const'; +import type { GroupingAggregation, GroupPanelRenderer } from './types'; import { GroupStatsRenderer, OnGroupToggle } from './types'; +import { getTelemetryEvent } from '../telemetry/const'; export interface GroupingProps { - data?: GroupingAggregation & GroupingFieldTotalAggregation; - groupingId: string; + activePage: number; + data?: GroupingAggregation; groupPanelRenderer?: GroupPanelRenderer; groupSelector?: JSX.Element; // list of custom UI components which correspond to your custom rendered metrics aggregations groupStatsRenderer?: GroupStatsRenderer; + groupingId: string; + groupingLevel?: number; inspectButton?: JSX.Element; isLoading: boolean; + itemsPerPage: number; + onChangeGroupsItemsPerPage?: (size: number) => void; + onChangeGroupsPage?: (index: number) => void; onGroupToggle?: OnGroupToggle; - pagination: { - pageIndex: number; - pageSize: number; - onChangeItemsPerPage: (itemsPerPageNumber: number) => void; - onChangePage: (pageNumber: number) => void; - itemsPerPageOptions: number[]; - }; renderChildComponent: (groupFilter: Filter[]) => React.ReactElement; + onGroupClose: () => void; selectedGroup: string; takeActionItems: (groupFilters: Filter[], groupNumber: number) => JSX.Element[]; tracker?: ( @@ -61,24 +55,29 @@ export interface GroupingProps { } const GroupingComponent = ({ + activePage, data, - groupingId, groupPanelRenderer, groupSelector, groupStatsRenderer, + groupingId, + groupingLevel = 0, inspectButton, isLoading, + itemsPerPage, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + onGroupClose, onGroupToggle, - pagination, renderChildComponent, selectedGroup, takeActionItems, tracker, unit = defaultUnit, }: GroupingProps) => { - const [trigger, setTrigger] = useState< - Record }> - >({}); + const [trigger, setTrigger] = useState>( + {} + ); const unitCount = data?.unitsCount?.value ?? 0; const unitCountText = useMemo(() => { @@ -100,16 +99,16 @@ const GroupingComponent = ({ return ( } forceState={(trigger[groupKey] && trigger[groupKey].state) ?? 'closed'} @@ -128,7 +127,6 @@ const GroupingComponent = ({ // ...trigger, -> this change will keep only one group at a time expanded and one table displayed [groupKey]: { state: isOpen ? 'open' : 'closed', - selectedBucket: groupBucket, }, }); onGroupToggle?.({ isOpen, groupName: group, groupNumber, groupingId }); @@ -139,8 +137,9 @@ const GroupingComponent = ({ : () => } selectedGroup={selectedGroup} + groupingLevel={groupingLevel} /> - + {groupingLevel > 0 ? null : } ); }), @@ -149,7 +148,9 @@ const GroupingComponent = ({ groupPanelRenderer, groupStatsRenderer, groupingId, + groupingLevel, isLoading, + onGroupClose, onGroupToggle, renderChildComponent, selectedGroup, @@ -159,58 +160,76 @@ const GroupingComponent = ({ ] ); const pageCount = useMemo( - () => (groupCount && pagination.pageSize ? Math.ceil(groupCount / pagination.pageSize) : 1), - [groupCount, pagination.pageSize] + () => (groupCount ? Math.ceil(groupCount / itemsPerPage) : 1), + [groupCount, itemsPerPage] ); + return ( <> - - - {groupCount > 0 && unitCount > 0 ? ( - - - - {unitCountText} - - - - - {groupCountText} - - + {groupingLevel > 0 ? null : ( + + + {groupCount > 0 && unitCount > 0 ? ( + + + + {unitCountText} + + + + + {groupCountText} + + + + ) : null} + + + + {inspectButton && {inspectButton}} + {groupSelector} - ) : null} - - - - {inspectButton && {inspectButton}} - {groupSelector} - - - -
+ + + )} +
0 ? groupingContainerCssLevel : groupingContainerCss} + className="eui-xScroll" + > {isLoading && ( )} {groupCount > 0 ? ( <> {groupPanels} - - + {groupCount > 0 && ( + <> + + { + if (onChangeGroupsItemsPerPage) { + onChangeGroupsItemsPerPage(pageSize); + } + }} + onChangePage={(pageIndex: number) => { + if (onChangeGroupsPage) { + onChangeGroupsPage(pageIndex); + } + }} + pageCount={pageCount} + showPerPageOptions + /> + + )} ) : ( diff --git a/packages/kbn-securitysolution-grouping/src/components/index.tsx b/packages/kbn-securitysolution-grouping/src/components/index.tsx index c924da988b04e4..0d759c0be48bea 100644 --- a/packages/kbn-securitysolution-grouping/src/components/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/index.tsx @@ -14,8 +14,9 @@ export * from './grouping'; /** * Checks if no group is selected - * @param groupKey selected group field value + * @param groupKeys selected group field values * * @returns {boolean} True if no group is selected */ -export const isNoneGroup = (groupKey: string | null) => groupKey === NONE_GROUP_KEY; +export const isNoneGroup = (groupKeys: string[]) => + !!groupKeys.find((groupKey) => groupKey === NONE_GROUP_KEY); diff --git a/packages/kbn-securitysolution-grouping/src/components/styles.tsx b/packages/kbn-securitysolution-grouping/src/components/styles.tsx index abf3d83db508e1..fcd2e4a61aaee9 100644 --- a/packages/kbn-securitysolution-grouping/src/components/styles.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/styles.tsx @@ -36,7 +36,7 @@ export const statsContainerCss = css` `; export const groupingContainerCss = css` - .euiAccordion__childWrapper .euiAccordion__padding--m { + .groupingAccordionForm .euiAccordion__childWrapper .euiAccordion__padding--m { margin-left: 8px; margin-right: 8px; border-left: ${euiThemeVars.euiBorderThin}; @@ -44,7 +44,7 @@ export const groupingContainerCss = css` border-bottom: ${euiThemeVars.euiBorderThin}; border-radius: 0 0 6px 6px; } - .euiAccordion__triggerWrapper { + .groupingAccordionForm .euiAccordion__triggerWrapper { border-bottom: ${euiThemeVars.euiBorderThin}; border-left: ${euiThemeVars.euiBorderThin}; border-right: ${euiThemeVars.euiBorderThin}; @@ -59,8 +59,37 @@ export const groupingContainerCss = css` border-radius: 6px; min-width: 1090px; } - .groupingAccordionForm__button { - text-decoration: none !important; + .groupingPanelRenderer { + display: table; + table-layout: fixed; + width: 100%; + padding-right: 32px; + } +`; + +export const groupingContainerCssLevel = css` + .groupingAccordionFormLevel .euiAccordion__childWrapper .euiAccordion__padding--m { + margin-left: 8px; + margin-right: 8px; + border-left: none; + border-right: none; + border-bottom: ${euiThemeVars.euiBorderThin}; + border-radius: 0; + } + .groupingAccordionFormLevel .euiAccordion__triggerWrapper { + border-bottom: ${euiThemeVars.euiBorderThin}; + border-left: none; + border-right: none; + min-height: 78px; + padding-left: 16px; + padding-right: 16px; + border-radius: 0; + } + .groupingAccordionFormLevel { + border-top: none; + border-bottom: none; + border-radius: 0; + min-width: 1090px; } .groupingPanelRenderer { display: table; diff --git a/packages/kbn-securitysolution-grouping/src/components/translations.ts b/packages/kbn-securitysolution-grouping/src/components/translations.ts index e3896d25b910f7..f2cb8a172dbdc6 100644 --- a/packages/kbn-securitysolution-grouping/src/components/translations.ts +++ b/packages/kbn-securitysolution-grouping/src/components/translations.ts @@ -35,9 +35,11 @@ export const GROUP_BY_CUSTOM_FIELD = i18n.translate('grouping.customGroupByPanel defaultMessage: 'Group By Custom Field', }); -export const SELECT_FIELD = i18n.translate('grouping.groupByPanelTitle', { - defaultMessage: 'Select Field', -}); +export const SELECT_FIELD = (groupingLevelsCount: number) => + i18n.translate('grouping.groupByPanelTitle', { + values: { groupingLevelsCount }, + defaultMessage: 'Select up to {groupingLevelsCount} groupings', + }); export const NONE = i18n.translate('grouping.noneGroupByOptionName', { defaultMessage: 'None', diff --git a/packages/kbn-securitysolution-grouping/src/components/types.ts b/packages/kbn-securitysolution-grouping/src/components/types.ts index 8956056581cc65..cf5f55f5c27f3e 100644 --- a/packages/kbn-securitysolution-grouping/src/components/types.ts +++ b/packages/kbn-securitysolution-grouping/src/components/types.ts @@ -19,7 +19,7 @@ export type RawBucket = GenericBuckets & T; /** Defines the shape of the aggregation returned by Elasticsearch */ // TODO: write developer docs for these fields -export interface GroupingAggregation { +export interface RootAggregation { groupByFields?: { buckets?: Array>; }; @@ -39,6 +39,8 @@ export type GroupingFieldTotalAggregation = Record< } >; +export type GroupingAggregation = RootAggregation & GroupingFieldTotalAggregation; + export interface BadgeMetric { value: number; color?: string; @@ -67,3 +69,5 @@ export type OnGroupToggle = (params: { groupNumber: number; groupingId: string; }) => void; + +export type { GroupingProps } from './grouping'; diff --git a/packages/kbn-securitysolution-grouping/src/containers/query/index.ts b/packages/kbn-securitysolution-grouping/src/containers/query/index.ts index 986788bf0dfa0b..23699c1ccf94a9 100644 --- a/packages/kbn-securitysolution-grouping/src/containers/query/index.ts +++ b/packages/kbn-securitysolution-grouping/src/containers/query/index.ts @@ -35,10 +35,10 @@ export const getGroupingQuery = ({ additionalFilters = [], from, groupByFields, - pageNumber, rootAggregations, runtimeMappings, size = DEFAULT_GROUP_BY_FIELD_SIZE, + pageNumber, sort, statsAggregations, to, diff --git a/packages/kbn-securitysolution-grouping/src/containers/query/types.ts b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts index c56e26223550a2..5a8b2f822fb5c8 100644 --- a/packages/kbn-securitysolution-grouping/src/containers/query/types.ts +++ b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts @@ -24,9 +24,10 @@ export interface GroupingQueryArgs { additionalFilters: BoolAgg[]; from: string; groupByFields: string[]; - pageNumber?: number; rootAggregations?: NamedAggregation[]; runtimeMappings?: MappingRuntimeFields; + additionalAggregationsRoot?: NamedAggregation[]; + pageNumber?: number; size?: number; sort?: Array<{ [category: string]: { order: 'asc' | 'desc' } }>; statsAggregations?: NamedAggregation[]; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts index 0953e6872ee91c..e1bdb08500fa84 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts @@ -6,55 +6,20 @@ * Side Public License, v 1. */ -import { - ActionType, - GroupOption, - UpdateActiveGroup, - UpdateGroupActivePage, - UpdateGroupItemsPerPage, - UpdateGroupOptions, -} from '../types'; +import { ActionType, GroupOption, UpdateActiveGroups, UpdateGroupOptions } from '../types'; -const updateActiveGroup = ({ - activeGroup, +const updateActiveGroups = ({ + activeGroups, id, }: { - activeGroup: string; + activeGroups: string[]; id: string; -}): UpdateActiveGroup => ({ +}): UpdateActiveGroups => ({ payload: { - activeGroup, + activeGroups, id, }, - type: ActionType.updateActiveGroup, -}); - -const updateGroupActivePage = ({ - activePage, - id, -}: { - activePage: number; - id: string; -}): UpdateGroupActivePage => ({ - payload: { - activePage, - id, - }, - type: ActionType.updateGroupActivePage, -}); - -const updateGroupItemsPerPage = ({ - itemsPerPage, - id, -}: { - itemsPerPage: number; - id: string; -}): UpdateGroupItemsPerPage => ({ - payload: { - itemsPerPage, - id, - }, - type: ActionType.updateGroupItemsPerPage, + type: ActionType.updateActiveGroups, }); const updateGroupOptions = ({ @@ -72,8 +37,6 @@ const updateGroupOptions = ({ }); export const groupActions = { - updateActiveGroup, - updateGroupActivePage, - updateGroupItemsPerPage, + updateActiveGroups, updateGroupOptions, }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts index 5348731d391285..5a1b4112df3aa9 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts @@ -24,7 +24,7 @@ const groupById = { [groupingId]: { ...defaultGroup, options: groupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; @@ -54,7 +54,7 @@ describe('grouping reducer', () => { JSON.stringify(groupingState.groupById) ); }); - it('updateActiveGroup', () => { + it('updateActiveGroups', () => { const { result } = renderHook(() => useReducer(groupsReducerWithStorage, { ...initialState, @@ -62,40 +62,11 @@ describe('grouping reducer', () => { }) ); let [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activeGroup).toEqual('host.name'); + expect(groupingState.groupById[groupingId].activeGroups).toEqual(['host.name']); act(() => { - dispatch(groupActions.updateActiveGroup({ id: groupingId, activeGroup: 'user.name' })); + dispatch(groupActions.updateActiveGroups({ id: groupingId, activeGroups: ['user.name'] })); }); [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activeGroup).toEqual('user.name'); - }); - it('updateGroupActivePage', () => { - const { result } = renderHook(() => - useReducer(groupsReducerWithStorage, { - ...initialState, - groupById, - }) - ); - let [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activePage).toEqual(0); - act(() => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: 12 })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activePage).toEqual(12); - }); - it('updateGroupItemsPerPage', () => { - const { result } = renderHook(() => useReducer(groupsReducerWithStorage, initialState)); - let [groupingState, dispatch] = result.current; - act(() => { - dispatch(groupActions.updateGroupOptions({ id: groupingId, newOptionList: groupingOptions })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].itemsPerPage).toEqual(25); - act(() => { - dispatch(groupActions.updateGroupItemsPerPage({ id: groupingId, itemsPerPage: 12 })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].itemsPerPage).toEqual(12); + expect(groupingState.groupById[groupingId].activeGroups).toEqual(['user.name']); }); }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts index 287227b5763b32..d59637b69defe2 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts @@ -25,8 +25,8 @@ export const initialState: GroupMap = { const groupsReducer = (state: GroupMap, action: Action, groupsById: GroupsById) => { switch (action.type) { - case ActionType.updateActiveGroup: { - const { id, activeGroup } = action.payload; + case ActionType.updateActiveGroups: { + const { id, activeGroups } = action.payload; return { ...state, groupById: { @@ -34,35 +34,7 @@ const groupsReducer = (state: GroupMap, action: Action, groupsById: GroupsById) [id]: { ...defaultGroup, ...groupsById[id], - activeGroup, - }, - }, - }; - } - case ActionType.updateGroupActivePage: { - const { id, activePage } = action.payload; - return { - ...state, - groupById: { - ...groupsById, - [id]: { - ...defaultGroup, - ...groupsById[id], - activePage, - }, - }, - }; - } - case ActionType.updateGroupItemsPerPage: { - const { id, itemsPerPage } = action.payload; - return { - ...state, - groupById: { - ...groupsById, - [id]: { - ...defaultGroup, - ...groupsById[id], - itemsPerPage, + activeGroups, }, }, }; @@ -89,22 +61,10 @@ export const groupsReducerWithStorage = (state: GroupMap, action: Action) => { if (storage) { groupsInStorage = getAllGroupsInStorage(storage); } - const trackedGroupIds = Object.keys(state.groupById); - - const adjustedStorageGroups = Object.entries(groupsInStorage).reduce( - (acc: GroupsById, [key, group]) => ({ - ...acc, - [key]: { - // reset page to 0 if is initial state - ...(trackedGroupIds.includes(key) ? group : { ...group, activePage: 0 }), - }, - }), - {} as GroupsById - ); const groupsById: GroupsById = { ...state.groupById, - ...adjustedStorageGroups, + ...groupsInStorage, }; const newState = groupsReducer(state, action, groupsById); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/types.ts b/packages/kbn-securitysolution-grouping/src/hooks/types.ts index 4b5480794b30dd..5c3e85d211eaf8 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/types.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/types.ts @@ -8,35 +8,21 @@ // action types export enum ActionType { - updateActiveGroup = 'UPDATE_ACTIVE_GROUP', - updateGroupActivePage = 'UPDATE_GROUP_ACTIVE_PAGE', - updateGroupItemsPerPage = 'UPDATE_GROUP_ITEMS_PER_PAGE', + updateActiveGroups = 'UPDATE_ACTIVE_GROUPS', updateGroupOptions = 'UPDATE_GROUP_OPTIONS', } -export interface UpdateActiveGroup { - type: ActionType.updateActiveGroup; - payload: { activeGroup: string; id: string }; +export interface UpdateActiveGroups { + type: ActionType.updateActiveGroups; + payload: { activeGroups: string[]; id: string }; } -export interface UpdateGroupActivePage { - type: ActionType.updateGroupActivePage; - payload: { activePage: number; id: string }; -} -export interface UpdateGroupItemsPerPage { - type: ActionType.updateGroupItemsPerPage; - payload: { itemsPerPage: number; id: string }; -} export interface UpdateGroupOptions { type: ActionType.updateGroupOptions; payload: { newOptionList: GroupOption[]; id: string }; } -export type Action = - | UpdateActiveGroup - | UpdateGroupActivePage - | UpdateGroupItemsPerPage - | UpdateGroupOptions; +export type Action = UpdateActiveGroups | UpdateGroupOptions; // state @@ -46,10 +32,8 @@ export interface GroupOption { } export interface GroupModel { - activeGroup: string; + activeGroups: string[]; options: GroupOption[]; - activePage: number; - itemsPerPage: number; } export interface GroupsById { @@ -73,8 +57,6 @@ export interface Storage { export const EMPTY_GROUP_BY_ID: GroupsById = {}; export const defaultGroup: GroupModel = { - activePage: 0, - itemsPerPage: 25, - activeGroup: 'none', + activeGroups: ['none'], options: [], }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx index d29313b36e518d..d741e7d15e6703 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx @@ -52,7 +52,7 @@ describe('useGetGroupSelector', () => { useGetGroupSelector({ ...defaultArgs, groupingState: { - groupById: { [groupingId]: { ...defaultGroup, activeGroup: customField } }, + groupById: { [groupingId]: { ...defaultGroup, activeGroups: [customField] } }, }, }) ); @@ -72,12 +72,12 @@ describe('useGetGroupSelector', () => { }); }); - it('On group change, does nothing when set to prev selected group', () => { + it('On group change, removes selected group if already selected', () => { const testGroup = { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -89,15 +89,22 @@ describe('useGetGroupSelector', () => { }, }); act(() => result.current.props.onGroupChange('host.name')); - expect(dispatch).toHaveBeenCalledTimes(0); + + expect(dispatch).toHaveBeenCalledWith({ + payload: { + id: groupingId, + activeGroups: ['none'], + }, + type: ActionType.updateActiveGroups, + }); }); - it('On group change, resets active page, sets active group, and leaves options alone', () => { + it('On group change to none, remove all previously selected groups', () => { const testGroup = { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name', 'user.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -108,22 +115,43 @@ describe('useGetGroupSelector', () => { }, }, }); - act(() => result.current.props.onGroupChange('user.name')); - expect(dispatch).toHaveBeenNthCalledWith(1, { + act(() => result.current.props.onGroupChange('none')); + + expect(dispatch).toHaveBeenCalledWith({ payload: { id: groupingId, - activePage: 0, + activeGroups: ['none'], }, - type: ActionType.updateGroupActivePage, + type: ActionType.updateActiveGroups, }); - expect(dispatch).toHaveBeenNthCalledWith(2, { + }); + + it('On group change, resets active page, sets active group, and leaves options alone', () => { + const testGroup = { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name'], + }, + }; + const { result } = renderHook((props) => useGetGroupSelector(props), { + initialProps: { + ...defaultArgs, + groupingState: { + groupById: testGroup, + }, + }, + }); + act(() => result.current.props.onGroupChange('user.name')); + + expect(dispatch).toHaveBeenNthCalledWith(1, { payload: { id: groupingId, - activeGroup: 'user.name', + activeGroups: ['host.name', 'user.name'], }, - type: ActionType.updateActiveGroup, + type: ActionType.updateActiveGroups, }); - expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenCalledTimes(1); }); it('On group change, sends telemetry', () => { @@ -131,7 +159,7 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -155,7 +183,7 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -179,10 +207,10 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; - const { result } = renderHook((props) => useGetGroupSelector(props), { + const { result, rerender } = renderHook((props) => useGetGroupSelector(props), { initialProps: { ...defaultArgs, groupingState: { @@ -191,17 +219,54 @@ describe('useGetGroupSelector', () => { }, }); act(() => result.current.props.onGroupChange(customField)); - expect(dispatch).toHaveBeenCalledTimes(3); - expect(dispatch).toHaveBeenNthCalledWith(3, { + expect(dispatch).toHaveBeenCalledTimes(1); + rerender({ + ...defaultArgs, + groupingState: { + groupById: { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name', customField], + }, + }, + }, + }); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(2, { + payload: { + newOptionList: [...defaultGroupingOptions, { label: customField, key: customField }], + id: 'test-table', + }, + type: ActionType.updateGroupOptions, + }); + }); + + it('Supports multiple custom fields on initial load', () => { + const testGroup = { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name', customField, 'another.custom'], + }, + }; + renderHook((props) => useGetGroupSelector(props), { + initialProps: { + ...defaultArgs, + groupingState: { + groupById: testGroup, + }, + }, + }); + expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledWith({ payload: { - id: groupingId, newOptionList: [ ...defaultGroupingOptions, - { - label: customField, - key: customField, - }, + { label: customField, key: customField }, + { label: 'another.custom', key: 'another.custom' }, ], + id: 'test-table', }, type: ActionType.updateGroupOptions, }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx index e3b1c45b2733d6..05920beb37a47d 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx @@ -22,6 +22,7 @@ export interface UseGetGroupSelectorArgs { fields: FieldSpec[]; groupingId: string; groupingState: GroupMap; + maxGroupingLevels?: number; onGroupChange?: (param: { groupByField: string; tableId: string }) => void; tracker?: ( type: UiCounterMetricType, @@ -36,22 +37,21 @@ export const useGetGroupSelector = ({ fields, groupingId, groupingState, + maxGroupingLevels = 1, onGroupChange, tracker, }: UseGetGroupSelectorArgs) => { - const { activeGroup: selectedGroup, options } = + const { activeGroups: selectedGroups, options } = groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup; - const setGroupsActivePage = useCallback( - (activePage: number) => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage })); - }, - [dispatch, groupingId] - ); - - const setSelectedGroup = useCallback( - (activeGroup: string) => { - dispatch(groupActions.updateActiveGroup({ id: groupingId, activeGroup })); + const setSelectedGroups = useCallback( + (activeGroups: string[]) => { + dispatch( + groupActions.updateActiveGroups({ + id: groupingId, + activeGroups, + }) + ); }, [dispatch, groupingId] ); @@ -65,11 +65,20 @@ export const useGetGroupSelector = ({ const onChange = useCallback( (groupSelection: string) => { - if (groupSelection === selectedGroup) { + if (selectedGroups.find((selected) => selected === groupSelection)) { + const groups = selectedGroups.filter((selectedGroup) => selectedGroup !== groupSelection); + if (groups.length === 0) { + setSelectedGroups(['none']); + } else { + setSelectedGroups(groups); + } return; } - setGroupsActivePage(0); - setSelectedGroup(groupSelection); + + const newSelectedGroups = isNoneGroup([groupSelection]) + ? [groupSelection] + : [...selectedGroups.filter((selectedGroup) => selectedGroup !== 'none'), groupSelection]; + setSelectedGroups(newSelectedGroups); // built-in telemetry: UI-counter tracker?.( @@ -78,62 +87,57 @@ export const useGetGroupSelector = ({ ); onGroupChange?.({ tableId: groupingId, groupByField: groupSelection }); - - // only update options if the new selection is a custom field - if ( - !isNoneGroup(groupSelection) && - !options.find((o: GroupOption) => o.key === groupSelection) - ) { - setOptions([ - ...defaultGroupingOptions, - { - label: groupSelection, - key: groupSelection, - }, - ]); - } }, - [ - defaultGroupingOptions, - groupingId, - onGroupChange, - options, - selectedGroup, - setGroupsActivePage, - setOptions, - setSelectedGroup, - tracker, - ] + [groupingId, onGroupChange, selectedGroups, setSelectedGroups, tracker] ); useEffect(() => { - // only set options the first time, all other updates will be taken care of by onGroupChange - if (options.length > 0) return; - setOptions( - defaultGroupingOptions.find((o) => o.key === selectedGroup) - ? defaultGroupingOptions - : [ - ...defaultGroupingOptions, - ...(!isNoneGroup(selectedGroup) - ? [ - { + if (options.length === 0) { + return setOptions( + defaultGroupingOptions.find((o) => selectedGroups.find((selected) => selected === o.key)) + ? defaultGroupingOptions + : [ + ...defaultGroupingOptions, + ...(!isNoneGroup(selectedGroups) + ? selectedGroups.map((selectedGroup) => ({ key: selectedGroup, label: selectedGroup, - }, - ] - : []), - ] - ); - }, [defaultGroupingOptions, options.length, selectedGroup, setOptions]); + })) + : []), + ] + ); + } + if (isNoneGroup(selectedGroups)) { + return; + } + + const currentOptionKeys = options.map((o) => o.key); + const newOptions = [...options]; + selectedGroups.forEach((groupSelection) => { + if (currentOptionKeys.includes(groupSelection)) { + return; + } + // these are custom fields + newOptions.push({ + label: groupSelection, + key: groupSelection, + }); + }); + + if (newOptions.length !== options.length) { + setOptions(newOptions); + } + }, [defaultGroupingOptions, options, selectedGroups, setOptions]); return ( diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx index a2a9eeec8bf204..d95ae866d2ee9e 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx @@ -30,7 +30,6 @@ const defaultArgs = { groupStatsRenderer: jest.fn(), inspectButton: <>, onGroupToggle: jest.fn(), - renderChildComponent: () =>

{'hello'}

, }, }; @@ -38,6 +37,9 @@ const groupingArgs = { data: {}, isLoading: false, takeActionItems: jest.fn(), + activePage: 0, + itemsPerPage: 25, + onGroupClose: () => {}, }; describe('useGrouping', () => { @@ -70,6 +72,8 @@ describe('useGrouping', () => { value: 18, }, }, + renderChildComponent: () =>

{'hello'}

, + selectedGroup: 'none', })} ); @@ -84,7 +88,7 @@ describe('useGrouping', () => { getItem.mockReturnValue( JSON.stringify({ 'test-table': { - activePage: 0, + itemsPerPageOptions: [10, 25, 50, 100], itemsPerPage: 25, activeGroup: 'kibana.alert.rule.name', options: defaultGroupingOptions, @@ -95,7 +99,7 @@ describe('useGrouping', () => { const { result, waitForNextUpdate } = renderHook(() => useGrouping(defaultArgs)); await waitForNextUpdate(); await waitForNextUpdate(); - const { getByTestId, queryByTestId } = render( + const { getByTestId } = render( {result.current.getGrouping({ ...groupingArgs, @@ -119,12 +123,13 @@ describe('useGrouping', () => { value: 18, }, }, + renderChildComponent: jest.fn(), + selectedGroup: 'test', })} ); expect(getByTestId('grouping-table')).toBeInTheDocument(); - expect(queryByTestId('innerTable')).not.toBeInTheDocument(); }); }); }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx index 993809943252f9..5833ae8205d596 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx @@ -11,8 +11,7 @@ import React, { useCallback, useMemo, useReducer } from 'react'; import { UiCounterMetricType } from '@kbn/analytics'; import { groupsReducerWithStorage, initialState } from './state/reducer'; import { GroupingProps, GroupSelectorProps, isNoneGroup } from '..'; -import { useGroupingPagination } from './use_grouping_pagination'; -import { groupActions, groupByIdSelector } from './state'; +import { groupByIdSelector } from './state'; import { useGetGroupSelector } from './use_get_group_selector'; import { defaultGroup, GroupOption } from './types'; import { Grouping as GroupingComponent } from '../components/grouping'; @@ -23,33 +22,37 @@ import { Grouping as GroupingComponent } from '../components/grouping'; interface Grouping { getGrouping: (props: DynamicGroupingProps) => React.ReactElement; groupSelector: React.ReactElement; - pagination: { - reset: () => void; - pageIndex: number; - pageSize: number; - }; - selectedGroup: string; + selectedGroups: string[]; } -/** Type for static grouping component props where T is the `GroupingAggregation` +/** Type for static grouping component props where T is the consumer `GroupingAggregation` * @interface StaticGroupingProps */ type StaticGroupingProps = Pick< GroupingProps, - | 'groupPanelRenderer' - | 'groupStatsRenderer' - | 'inspectButton' - | 'onGroupToggle' - | 'renderChildComponent' - | 'unit' + 'groupPanelRenderer' | 'groupStatsRenderer' | 'onGroupToggle' | 'unit' >; -/** Type for dynamic grouping component props where T is the `GroupingAggregation` +/** Type for dynamic grouping component props where T is the consumer `GroupingAggregation` * @interface DynamicGroupingProps */ -type DynamicGroupingProps = Pick, 'data' | 'isLoading' | 'takeActionItems'>; +export type DynamicGroupingProps = Pick< + GroupingProps, + | 'activePage' + | 'data' + | 'groupingLevel' + | 'inspectButton' + | 'isLoading' + | 'itemsPerPage' + | 'onChangeGroupsItemsPerPage' + | 'onChangeGroupsPage' + | 'renderChildComponent' + | 'onGroupClose' + | 'selectedGroup' + | 'takeActionItems' +>; -/** Interface for configuring grouping package where T is the `GroupingAggregation` +/** Interface for configuring grouping package where T is the consumer `GroupingAggregation` * @interface GroupingArgs */ interface GroupingArgs { @@ -57,6 +60,7 @@ interface GroupingArgs { defaultGroupingOptions: GroupOption[]; fields: FieldSpec[]; groupingId: string; + maxGroupingLevels?: number; /** for tracking * @param param { groupByField: string; tableId: string } selected group and table id */ @@ -75,21 +79,22 @@ interface GroupingArgs { * @param defaultGroupingOptions defines the grouping options as an array of {@link GroupOption} * @param fields FieldSpec array serialized version of DataViewField fields. Available in the custom grouping options * @param groupingId Unique identifier of the grouping component. Used in local storage + * @param maxGroupingLevels maximum group nesting levels (optional) * @param onGroupChange callback executed when selected group is changed, used for tracking * @param tracker telemetry handler - * @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroup } + * @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroups } */ export const useGrouping = ({ componentProps, defaultGroupingOptions, fields, groupingId, + maxGroupingLevels, onGroupChange, tracker, }: GroupingArgs): Grouping => { const [groupingState, dispatch] = useReducer(groupsReducerWithStorage, initialState); - - const { activeGroup: selectedGroup } = useMemo( + const { activeGroups: selectedGroups } = useMemo( () => groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup, [groupingId, groupingState] ); @@ -100,56 +105,37 @@ export const useGrouping = ({ fields, groupingId, groupingState, + maxGroupingLevels, onGroupChange, tracker, }); - const pagination = useGroupingPagination({ groupingId, groupingState, dispatch }); - const getGrouping = useCallback( /** * * @param props {@link DynamicGroupingProps} */ (props: DynamicGroupingProps): React.ReactElement => - isNoneGroup(selectedGroup) ? ( - componentProps.renderChildComponent([]) + isNoneGroup([props.selectedGroup]) ? ( + props.renderChildComponent([]) ) : ( ), - [componentProps, groupSelector, groupingId, pagination, selectedGroup, tracker] + [componentProps, groupSelector, groupingId, tracker] ); - const resetPagination = useCallback(() => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: 0 })); - }, [groupingId]); - return useMemo( () => ({ getGrouping, groupSelector, - selectedGroup, - pagination: { - reset: resetPagination, - pageIndex: pagination.pageIndex, - pageSize: pagination.pageSize, - }, + selectedGroups, }), - [ - getGrouping, - groupSelector, - pagination.pageIndex, - pagination.pageSize, - resetPagination, - selectedGroup, - ] + [getGrouping, groupSelector, selectedGroups] ); }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts deleted file mode 100644 index 9aa07458aaf6fc..00000000000000 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts +++ /dev/null @@ -1,53 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { useCallback, useMemo } from 'react'; -import { groupActions, groupByIdSelector } from './state'; -import { Action, defaultGroup, GroupMap } from './types'; - -export interface UseGroupingPaginationArgs { - dispatch: React.Dispatch; - groupingId: string; - groupingState: GroupMap; -} - -export const useGroupingPagination = ({ - groupingId, - groupingState, - dispatch, -}: UseGroupingPaginationArgs) => { - const { activePage, itemsPerPage } = - groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup; - - const setGroupsActivePage = useCallback( - (newActivePage: number) => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: newActivePage })); - }, - [dispatch, groupingId] - ); - - const setGroupsItemsPerPage = useCallback( - (newItemsPerPage: number) => { - dispatch( - groupActions.updateGroupItemsPerPage({ id: groupingId, itemsPerPage: newItemsPerPage }) - ); - }, - [dispatch, groupingId] - ); - - return useMemo( - () => ({ - pageIndex: activePage, - pageSize: itemsPerPage, - onChangeItemsPerPage: setGroupsItemsPerPage, - onChangePage: setGroupsActivePage, - itemsPerPageOptions: [10, 25, 50, 100], - }), - [activePage, itemsPerPage, setGroupsActivePage, setGroupsItemsPerPage] - ); -}; diff --git a/packages/kbn-securitysolution-grouping/tsconfig.json b/packages/kbn-securitysolution-grouping/tsconfig.json index ab98ec47e3c93d..621ba68957cf1f 100644 --- a/packages/kbn-securitysolution-grouping/tsconfig.json +++ b/packages/kbn-securitysolution-grouping/tsconfig.json @@ -6,7 +6,8 @@ "jest", "node", "react", - "@emotion/react/types/css-prop" + "@emotion/react/types/css-prop", + "@kbn/ambient-ui-types" ] }, "include": [ diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index da762f8f557259..c612ef05d4b7ac 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -37,6 +37,7 @@ export const storybookAliases = { expression_shape: 'src/plugins/expression_shape/.storybook', expression_tagcloud: 'src/plugins/chart_expressions/expression_tagcloud/.storybook', fleet: 'x-pack/plugins/fleet/.storybook', + grouping: 'packages/kbn-securitysolution-grouping/.storybook', home: 'src/plugins/home/.storybook', infra: 'x-pack/plugins/infra/.storybook', kibana_react: 'src/plugins/kibana_react/.storybook', diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx index 5a3f4b3e25e0ed..07342c4f60691e 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { SecurityPageName } from '../../../../common/constants'; -import { useGlobalTime } from '../../containers/use_global_time'; import { DEFAULT_STACK_BY_FIELD, DEFAULT_STACK_BY_FIELD1, @@ -151,16 +150,6 @@ describe('AlertsTreemapPanel', () => { await waitFor(() => expect(screen.getByTestId('treemapPanel')).toBeInTheDocument()); }); - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', async () => { - render( - - - - ); - - await waitFor(() => expect(useGlobalTime).toBeCalledWith(false)); - }); - it('renders the panel with a hidden overflow-x', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx index a3ba582b3c974a..33e526932c3e6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx @@ -80,7 +80,7 @@ const AlertsTreemapPanelComponent: React.FC = ({ stackByWidth, title, }: Props) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${ALERTS_TREEMAP_ID}-${uuidv4()}`, []); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index e1b5f31f2d3b58..0113e0e584b54b 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -110,7 +110,7 @@ const StatefulTopNComponent: React.FC = ({ value, }) => { const { uiSettings } = useKibana().services; - const { from, deleteQuery, setQuery, to } = useGlobalTime(false); + const { from, deleteQuery, setQuery, to } = useGlobalTime(); const options = getOptions(isActiveTimeline(scopeId ?? '') ? activeTimelineEventType : undefined); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts deleted file mode 100644 index b1a21d9ae492d5..00000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts +++ /dev/null @@ -1,46 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { renderHook } from '@testing-library/react-hooks'; - -import { TestProviders } from '../../mock'; -import { useAlertPrevalence } from './use_alert_prevalence'; -import { useGlobalTime } from '../use_global_time'; - -const from = '2022-07-28T08:20:18.966Z'; -const to = '2022-07-28T08:20:18.966Z'; -jest.mock('../use_global_time', () => { - const actual = jest.requireActual('../use_global_time'); - return { - ...actual, - useGlobalTime: jest - .fn() - .mockReturnValue({ from, to, setQuery: jest.fn(), deleteQuery: jest.fn() }), - }; -}); - -describe('useAlertPrevalence', () => { - beforeEach(() => jest.resetAllMocks()); - - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', () => { - renderHook( - () => - useAlertPrevalence({ - field: 'host.name', - value: ['Host-byc3w6qlpo'], - isActiveTimelines: false, - signalIndexName: null, - includeAlertIds: false, - }), - { - wrapper: TestProviders, - } - ); - - expect(useGlobalTime).toBeCalledWith(false); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts index 03ac3d61693510..3e98e067bfe2dd 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts +++ b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts @@ -44,7 +44,7 @@ export const useAlertPrevalence = ({ const timelineTime = useDeepEqualSelector((state) => inputsSelectors.timelineTimeRangeSelector(state) ); - const globalTime = useGlobalTime(false); + const globalTime = useGlobalTime(); let to: string | undefined; let from: string | undefined; if (ignoreTimerange === false) { diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx index 480ecdb3674fff..46a2738d6247a1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx @@ -37,23 +37,77 @@ describe('useGlobalTime', () => { expect(result1.to).toBe(0); }); - test('clear all queries at unmount when clearAllQuery is set to true', () => { - const { unmount } = renderHook(() => useGlobalTime()); + test('clear query at unmount when setQuery has been called', () => { + const { result, unmount } = renderHook(() => useGlobalTime()); + act(() => { + result.current.setQuery({ + id: 'query-2', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + unmount(); - expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/local/inputs/DELETE_ALL_QUERY' + expect(mockDispatch.mock.calls.length).toBe(2); + expect(mockDispatch.mock.calls[1][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' ); }); - test('do NOT clear all queries at unmount when clearAllQuery is set to false.', () => { - const { unmount } = renderHook(() => useGlobalTime(false)); + test('do NOT clear query at unmount when setQuery has not been called', () => { + const { unmount } = renderHook(() => useGlobalTime()); unmount(); expect(mockDispatch.mock.calls.length).toBe(0); }); - test('do NOT clear all queries when setting state and clearAllQuery is set to true', () => { - const { rerender } = renderHook(() => useGlobalTime()); - act(() => rerender()); - expect(mockDispatch.mock.calls.length).toBe(0); + test('do clears only the dismounted queries at unmount when setQuery is called', () => { + const { result, unmount } = renderHook(() => useGlobalTime()); + + act(() => { + result.current.setQuery({ + id: 'query-1', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + + act(() => { + result.current.setQuery({ + id: 'query-2', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + + const { result: theOneWillNotBeDismounted } = renderHook(() => useGlobalTime()); + + act(() => { + theOneWillNotBeDismounted.current.setQuery({ + id: 'query-3h', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + unmount(); + expect(mockDispatch).toHaveBeenCalledTimes(5); + expect(mockDispatch.mock.calls[3][0].payload.id).toEqual('query-1'); + + expect(mockDispatch.mock.calls[3][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' + ); + + expect(mockDispatch.mock.calls[4][0].payload.id).toEqual('query-2'); + + expect(mockDispatch.mock.calls[4][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx index dbb57d57c3e6e4..76cd23c8efba05 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx @@ -6,7 +6,7 @@ */ import { pick } from 'lodash/fp'; -import { useCallback, useState, useEffect, useMemo } from 'react'; +import { useCallback, useState, useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { InputsModelId } from '../../store/inputs/constants'; @@ -15,15 +15,18 @@ import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; import type { SetQuery, DeleteQuery } from './types'; -export const useGlobalTime = (clearAllQuery: boolean = true) => { +export const useGlobalTime = () => { const dispatch = useDispatch(); const { from, to } = useDeepEqualSelector((state) => pick(['from', 'to'], inputsSelectors.globalTimeRangeSelector(state)) ); const [isInitializing, setIsInitializing] = useState(true); + const queryId = useRef([]); + const setQuery = useCallback( - ({ id, inspect, loading, refetch, searchSessionId }: SetQuery) => + ({ id, inspect, loading, refetch, searchSessionId }: SetQuery) => { + queryId.current = [...queryId.current, id]; dispatch( inputsActions.setQuery({ inputId: InputsModelId.global, @@ -33,7 +36,8 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { refetch, searchSessionId, }) - ), + ); + }, [dispatch] ); @@ -50,13 +54,13 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { // This effect must not have any mutable dependencies. Otherwise, the cleanup function gets called before the component unmounts. useEffect(() => { return () => { - if (clearAllQuery) { - dispatch(inputsActions.deleteAllQuery({ id: InputsModelId.global })); + if (queryId.current.length > 0) { + queryId.current.forEach((id) => deleteQuery({ id })); } }; - }, [dispatch, clearAllQuery]); + }, [deleteQuery]); - const memoizedReturn = useMemo( + return useMemo( () => ({ isInitializing, from, @@ -66,8 +70,6 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { }), [deleteQuery, from, isInitializing, setQuery, to] ); - - return memoizedReturn; }; export type GlobalTimeArgs = Omit, 'deleteQuery'> & diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts b/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts index a61186aeb0f8f1..d78be8b03cb4d5 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts @@ -11,9 +11,5 @@ import type React from 'react'; const actionCreator = actionCreatorFactory('x-pack/security_solution/groups'); export const updateGroupSelector = actionCreator<{ - groupSelector: React.ReactElement; + groupSelector: React.ReactElement | null; }>('UPDATE_GROUP_SELECTOR'); - -export const updateSelectedGroup = actionCreator<{ - selectedGroup: string; -}>('UPDATE_SELECTED_GROUP'); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts b/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts index aaea793e4ca86d..6914e4ad465fee 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts @@ -6,20 +6,17 @@ */ import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { updateGroupSelector, updateSelectedGroup } from './actions'; +import { updateGroupSelector } from './actions'; import type { GroupModel } from './types'; export const initialGroupingState: GroupModel = { groupSelector: null, - selectedGroup: null, }; -export const groupsReducer = reducerWithInitialState(initialGroupingState) - .case(updateSelectedGroup, (state, { selectedGroup }) => ({ - ...state, - selectedGroup, - })) - .case(updateGroupSelector, (state, { groupSelector }) => ({ +export const groupsReducer = reducerWithInitialState(initialGroupingState).case( + updateGroupSelector, + (state, { groupSelector }) => ({ ...state, groupSelector, - })); + }) +); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts b/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts index eb63e256a4d9f0..126fdac8c1b36f 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts @@ -11,7 +11,3 @@ import type { GroupState } from './types'; const groupSelector = (state: GroupState) => state.groups.groupSelector; export const getGroupSelector = () => createSelector(groupSelector, (selector) => selector); - -export const selectedGroup = (state: GroupState) => state.groups.selectedGroup; - -export const getSelectedGroup = () => createSelector(selectedGroup, (group) => group); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/types.ts b/x-pack/plugins/security_solution/public/common/store/grouping/types.ts index 7d8fd4bc3eecab..d2250b15722edd 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/types.ts @@ -7,7 +7,6 @@ export interface GroupModel { groupSelector: React.ReactElement | null; - selectedGroup: string | null; } export interface GroupState { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 6e1b4fddbd167f..90f1d38f69774c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -852,7 +852,7 @@ const RuleDetailsPageComponent: React.FC = ({ {ruleId != null && ( { }); }); - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', async () => { - await act(async () => { - mount( - - - - ); - - expect(useGlobalTime).toBeCalledWith(false); - }); - }); - it('renders with the specified `alignHeader` alignment', async () => { await act(async () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 26eb4522ed6171..f9967a44ffb6e1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -84,7 +84,7 @@ export const AlertsCountPanel = memo( isExpanded, setIsExpanded, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled'); // create a unique, but stable (across re-renders) query id diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index d1cb85cdf45644..e1945ca151cd8b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -152,7 +152,7 @@ export const AlertsHistogramPanel = memo( isExpanded, setIsExpanded, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuidv4()}`, []); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx index fb5024d3c2e505..e8d0ddd061e81f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx @@ -82,7 +82,7 @@ export const useSummaryChartData: UseAlerts = ({ signalIndexName, skip = false, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const [updatedAt, setUpdatedAt] = useState(Date.now()); const [items, setItems] = useState([]); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx new file mode 100644 index 00000000000000..4a57d7cac8e737 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx @@ -0,0 +1,376 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, render, within } from '@testing-library/react'; +import type { Filter } from '@kbn/es-query'; +import useResizeObserver from 'use-resize-observer/polyfilled'; + +import '../../../common/mock/match_media'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../../common/mock'; +import type { AlertsTableComponentProps } from './alerts_grouping'; +import { GroupedAlertsTable } from './alerts_grouping'; +import { TableId } from '@kbn/securitysolution-data-table'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; +import { createStore } from '../../../common/store'; +import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__'; +import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { groupingSearchResponse } from './grouping_settings/mock'; + +jest.mock('../../containers/detection_engine/alerts/use_query'); +jest.mock('../../../common/containers/sourcerer'); +jest.mock('../../../common/utils/normalize_time_range'); +jest.mock('../../../common/containers/use_global_time', () => ({ + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), +})); + +const mockOptions = [ + { label: 'ruleName', key: 'kibana.alert.rule.name' }, + { label: 'userName', key: 'user.name' }, + { label: 'hostName', key: 'host.name' }, + { label: 'sourceIP', key: 'source.ip' }, +]; +// +jest.mock('./grouping_settings', () => { + const actual = jest.requireActual('./grouping_settings'); + + return { + ...actual, + getDefaultGroupingOptions: () => mockOptions, + }; +}); + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const mockUseFieldBrowserOptions = jest.fn(); +jest.mock('../../../timelines/components/fields_browser', () => ({ + useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), +})); + +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer/polyfilled'); +mockUseResizeObserver.mockImplementation(() => ({})); +const mockedUseKibana = mockUseKibana(); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../common/lib/kibana'); + + return { + ...original, + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + telemetry: mockedTelemetry, + }, + }), + }; +}); + +jest.mock('./timeline_actions/use_add_bulk_to_timeline', () => ({ + useAddBulkToTimelineAction: jest.fn(() => {}), +})); +const sourcererDataView = { + indicesExist: true, + loading: false, + indexPattern: { + fields: [], + }, + browserFields: {}, +}; +const renderChildComponent = (groupingFilters: Filter[]) =>

; + +const testProps: AlertsTableComponentProps = { + defaultFilters: [], + from: '2020-07-07T08:20:18.966Z', + globalFilters: [], + globalQuery: { + query: 'query', + language: 'language', + }, + hasIndexMaintenance: true, + hasIndexWrite: true, + loading: false, + renderChildComponent, + runtimeMappings: {}, + signalIndexName: 'test', + tableId: TableId.test, + to: '2020-07-08T08:20:18.966Z', +}; + +const mockUseQueryAlerts = useQueryAlerts as jest.Mock; +const mockQueryResponse = { + loading: false, + data: {}, + setQuery: () => {}, + response: '', + request: '', + refetch: () => {}, +}; + +const getMockStorageState = (groups: string[] = ['none']) => + JSON.stringify({ + [testProps.tableId]: { + activeGroups: groups, + options: mockOptions, + }, + }); + +describe('GroupedAlertsTable', () => { + const { storage } = createSecuritySolutionStorageMock(); + let store: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + selectedPatterns: ['myFakebeat-*'], + }); + mockUseQueryAlerts.mockImplementation((i) => { + if (i.skip) { + return mockQueryResponse; + } + if (i.query.aggs.groupByFields.multi_terms != null) { + return { + ...mockQueryResponse, + data: groupingSearchResponse.ruleName, + }; + } + return { + ...mockQueryResponse, + data: i.query.aggs.groupByFields.terms.field != null ? groupingSearchResponse.hostName : {}, + }; + }); + }); + + it('calls the proper initial dispatch actions for groups', () => { + const { getByTestId, queryByTestId } = render( + + + + ); + + expect(queryByTestId('empty-results-panel')).not.toBeInTheDocument(); + expect(queryByTestId('group-selector-dropdown')).not.toBeInTheDocument(); + expect(getByTestId('alerts-table')).toBeInTheDocument(); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch.mock.calls[0][0].type).toEqual( + 'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR' + ); + }); + + it('renders empty grouping table when group is selected without data', async () => { + mockUseQueryAlerts.mockReturnValue(mockQueryResponse); + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name'])); + const { getByTestId, queryByTestId } = render( + + + + ); + expect(queryByTestId('alerts-table')).not.toBeInTheDocument(); + expect(getByTestId('empty-results-panel')).toBeInTheDocument(); + }); + + it('renders grouping table in first accordion level when single group is selected', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name'])); + + const { getAllByTestId } = render( + + + + ); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + expect(within(level0).getByTestId('alerts-table')).toBeInTheDocument(); + }); + + it('renders grouping table in second accordion level when 2 groups are selected', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name'])); + + const { getAllByTestId } = render( + + + + ); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + expect(within(level0).queryByTestId('alerts-table')).not.toBeInTheDocument(); + + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + const level1 = within(getAllByTestId('grouping-accordion-content')[1]); + expect(level1.getByTestId('alerts-table')).toBeInTheDocument(); + }); + + it('resets all levels pagination when selected group changes', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + getByTestId('grouping-level-2-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual(null); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual('true'); + }); + + fireEvent.click(getAllByTestId('group-selector-dropdown')[0]); + fireEvent.click(getAllByTestId('panel-user.name')[0]); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + // level 2 has been removed with the group selection change + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual('true'); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual(null); + }); + }); + + it('resets all levels pagination when global query updates', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId, rerender } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + rerender( + + + + ); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + getByTestId('grouping-level-2-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual('true'); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual(null); + }); + }); + + it('resets only most inner group pagination when its parent groups open/close', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[28]); + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual(null); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual('true'); + }); + + expect( + within(getByTestId('grouping-level-2-pagination')) + .getByTestId('pagination-button-0') + .getAttribute('aria-current') + ).toEqual('true'); + expect( + within(getByTestId('grouping-level-2-pagination')) + .getByTestId('pagination-button-1') + .getAttribute('aria-current') + ).toEqual(null); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx index e5868970f6768c..99ae3f4ff3433a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx @@ -5,50 +5,29 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; -import { useDispatch } from 'react-redux'; -import { v4 as uuidv4 } from 'uuid'; +import { useDispatch, useSelector } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; -import { buildEsQuery } from '@kbn/es-query'; -import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import type { - GroupingFieldTotalAggregation, - GroupingAggregation, -} from '@kbn/securitysolution-grouping'; -import { useGrouping, isNoneGroup } from '@kbn/securitysolution-grouping'; +import type { GroupOption } from '@kbn/securitysolution-grouping'; +import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; +import { isEmpty, isEqual } from 'lodash/fp'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; -import type { AlertsGroupingAggregation } from './grouping_settings/types'; +import { groupSelectors } from '../../../common/store/grouping'; +import type { State } from '../../../common/store'; +import { updateGroupSelector } from '../../../common/store/grouping/actions'; import type { Status } from '../../../../common/detection_engine/schemas/common'; -import { InspectButton } from '../../../common/components/inspect'; import { defaultUnit } from '../../../common/components/toolbar/unit'; -import { useGlobalTime } from '../../../common/containers/use_global_time'; -import { combineQueries } from '../../../common/lib/kuery'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; -import { useKibana } from '../../../common/lib/kibana'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { useInspectButton } from '../alerts_kpis/common/hooks'; - -import { buildTimeRangeFilter } from './helpers'; -import * as i18n from './translations'; -import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; -import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; -import { - getAlertsGroupingQuery, - getDefaultGroupingOptions, - renderGroupPanel, - getStats, - useGroupTakeActionsItems, -} from './grouping_settings'; -import { updateGroupSelector, updateSelectedGroup } from '../../../common/store/grouping/actions'; +import { getDefaultGroupingOptions, renderGroupPanel, getStats } from './grouping_settings'; +import { useKibana } from '../../../common/lib/kibana'; +import { GroupedSubLevel } from './alerts_sub_grouping'; import { track } from '../../../common/lib/telemetry'; -const ALERTS_GROUPING_ID = 'alerts-grouping'; - export interface AlertsTableComponentProps { - currentAlertStatusFilterValue?: Status; + currentAlertStatusFilterValue?: Status[]; defaultFilters?: Filter[]; from: string; globalFilters: Filter[]; @@ -63,52 +42,37 @@ export interface AlertsTableComponentProps { to: string; } -export const GroupedAlertsTableComponent: React.FC = ({ - defaultFilters = [], - from, - globalFilters, - globalQuery, - hasIndexMaintenance, - hasIndexWrite, - loading, - tableId, - to, - runtimeMappings, - signalIndexName, - currentAlertStatusFilterValue, - renderChildComponent, -}) => { - const dispatch = useDispatch(); +const DEFAULT_PAGE_SIZE = 25; +const DEFAULT_PAGE_INDEX = 0; +const MAX_GROUPING_LEVELS = 3; - const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( - SourcererScopeName.detections +const useStorage = (storage: Storage, tableId: string) => + useMemo( + () => ({ + getStoragePageSize: (): number[] => { + const pageSizes = storage.get(`grouping-table-${tableId}`); + if (!pageSizes) { + return Array(MAX_GROUPING_LEVELS).fill(DEFAULT_PAGE_SIZE); + } + return pageSizes; + }, + setStoragePageSize: (pageSizes: number[]) => { + storage.set(`grouping-table-${tableId}`, pageSizes); + }, + }), + [storage, tableId] ); + +const GroupedAlertsTableComponent: React.FC = (props) => { + const dispatch = useDispatch(); + + const { indexPattern, selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); + const { - services: { uiSettings, telemetry }, + services: { storage, telemetry }, } = useKibana(); - const getGlobalQuery = useCallback( - (customFilters: Filter[]) => { - if (browserFields != null && indexPattern != null) { - return combineQueries({ - config: getEsQueryConfig(uiSettings), - dataProviders: [], - indexPattern, - browserFields, - filters: [ - ...(defaultFilters ?? []), - ...globalFilters, - ...customFilters, - ...buildTimeRangeFilter(from, to), - ], - kqlQuery: globalQuery, - kqlMode: globalQuery.language, - }); - } - return null; - }, - [browserFields, indexPattern, uiSettings, defaultFilters, globalFilters, from, to, globalQuery] - ); + const { getStoragePageSize, setStoragePageSize } = useStorage(storage, props.tableId); const { onGroupChange, onGroupToggle } = useMemo( () => ({ @@ -125,153 +89,146 @@ export const GroupedAlertsTableComponent: React.FC = [telemetry] ); - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []); - - const inspect = useMemo( - () => ( - - ), - [uniqueQueryId] - ); - - const { groupSelector, getGrouping, selectedGroup, pagination } = useGrouping({ + const { groupSelector, getGrouping, selectedGroups } = useGrouping({ componentProps: { groupPanelRenderer: renderGroupPanel, groupStatsRenderer: getStats, - inspectButton: inspect, onGroupToggle, - renderChildComponent, unit: defaultUnit, }, - defaultGroupingOptions: getDefaultGroupingOptions(tableId), + defaultGroupingOptions: getDefaultGroupingOptions(props.tableId), fields: indexPattern.fields, - groupingId: tableId, + groupingId: props.tableId, + maxGroupingLevels: MAX_GROUPING_LEVELS, onGroupChange, tracker: track, }); - const resetPagination = pagination.reset; - useEffect(() => { - dispatch(updateGroupSelector({ groupSelector })); - }, [dispatch, groupSelector]); + const getGroupSelector = groupSelectors.getGroupSelector(); - useEffect(() => { - dispatch(updateSelectedGroup({ selectedGroup })); - }, [dispatch, selectedGroup]); - - useInvalidFilterQuery({ - id: tableId, - filterQuery: getGlobalQuery([])?.filterQuery, - kqlError: getGlobalQuery([])?.kqlError, - query: globalQuery, - startDate: from, - endDate: to, - }); + const groupSelectorInRedux = useSelector((state: State) => getGroupSelector(state)); + const selectorOptions = useRef([]); - const { deleteQuery, setQuery } = useGlobalTime(false); - const additionalFilters = useMemo(() => { - resetPagination(); - try { - return [ - buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [ - ...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []), - ...(defaultFilters ?? []), - ]), - ]; - } catch (e) { - return []; + useEffect(() => { + if ( + isNoneGroup(selectedGroups) && + groupSelector.props.options.length > 0 && + (groupSelectorInRedux == null || + !isEqual(selectorOptions.current, groupSelector.props.options)) + ) { + selectorOptions.current = groupSelector.props.options; + dispatch(updateGroupSelector({ groupSelector })); + } else if (!isNoneGroup(selectedGroups) && groupSelectorInRedux !== null) { + dispatch(updateGroupSelector({ groupSelector: null })); } - }, [defaultFilters, globalFilters, globalQuery, resetPagination]); + }, [dispatch, groupSelector, groupSelectorInRedux, selectedGroups]); - const queryGroups = useMemo( - () => - getAlertsGroupingQuery({ - additionalFilters, - selectedGroup, - from, - runtimeMappings, - to, - pageSize: pagination.pageSize, - pageIndex: pagination.pageIndex, - }), - [ - additionalFilters, - selectedGroup, - from, - runtimeMappings, - to, - pagination.pageSize, - pagination.pageIndex, - ] + const [pageIndex, setPageIndex] = useState( + Array(MAX_GROUPING_LEVELS).fill(DEFAULT_PAGE_INDEX) ); + const [pageSize, setPageSize] = useState(getStoragePageSize); - const { - data: alertsGroupsData, - loading: isLoadingGroups, - refetch, - request, - response, - setQuery: setAlertsQuery, - } = useQueryAlerts< - {}, - GroupingAggregation & - GroupingFieldTotalAggregation - >({ - query: queryGroups, - indexName: signalIndexName, - queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING, - skip: isNoneGroup(selectedGroup), - }); + const resetAllPagination = useCallback(() => { + setPageIndex((curr) => curr.map(() => DEFAULT_PAGE_INDEX)); + }, []); useEffect(() => { - if (!isNoneGroup(selectedGroup)) { - setAlertsQuery(queryGroups); - } - }, [queryGroups, selectedGroup, setAlertsQuery]); + resetAllPagination(); + }, [resetAllPagination, selectedGroups]); + + const setPageVar = useCallback( + (newNumber: number, groupingLevel: number, pageType: 'index' | 'size') => { + if (pageType === 'index') { + setPageIndex((currentIndex) => { + const newArr = [...currentIndex]; + newArr[groupingLevel] = newNumber; + return newArr; + }); + } - useInspectButton({ - deleteQuery, - loading: isLoadingGroups, - response, - setQuery, - refetch, - request, - uniqueQueryId, - }); + if (pageType === 'size') { + setPageSize((currentIndex) => { + const newArr = [...currentIndex]; + newArr[groupingLevel] = newNumber; + setStoragePageSize(newArr); + return newArr; + }); + } + }, + [setStoragePageSize] + ); - const takeActionItems = useGroupTakeActionsItems({ - indexName: indexPattern.title, - currentStatus: currentAlertStatusFilterValue, - showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, + const nonGroupingFilters = useRef({ + defaultFilters: props.defaultFilters, + globalFilters: props.globalFilters, + globalQuery: props.globalQuery, }); - const getTakeActionItems = useCallback( - (groupFilters: Filter[], groupNumber: number) => - takeActionItems({ - query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, - tableId, - groupNumber, - selectedGroup, - }), - [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] - ); + useEffect(() => { + const nonGrouping = { + defaultFilters: props.defaultFilters, + globalFilters: props.globalFilters, + globalQuery: props.globalQuery, + }; + if (!isEqual(nonGroupingFilters.current, nonGrouping)) { + resetAllPagination(); + nonGroupingFilters.current = nonGrouping; + } + }, [props.defaultFilters, props.globalFilters, props.globalQuery, resetAllPagination]); + + const getLevel = useCallback( + (level: number, selectedGroup: string, parentGroupingFilter?: string) => { + let rcc; + if (level < selectedGroups.length - 1) { + rcc = (groupingFilters: Filter[]) => { + return getLevel( + level + 1, + selectedGroups[level + 1], + JSON.stringify([ + ...groupingFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]) + ); + }; + } else { + rcc = (groupingFilters: Filter[]) => { + return props.renderChildComponent([ + ...groupingFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]); + }; + } - const groupedAlerts = useMemo( - () => - getGrouping({ - data: alertsGroupsData?.aggregations, - isLoading: loading || isLoadingGroups, - takeActionItems: getTakeActionItems, - }), - [alertsGroupsData?.aggregations, getGrouping, getTakeActionItems, isLoadingGroups, loading] + const resetGroupChildrenPagination = (parentLevel: number) => { + setPageIndex((allPages) => { + const resetPages = allPages.splice(parentLevel + 1, allPages.length); + return [...allPages, ...resetPages.map(() => DEFAULT_PAGE_INDEX)]; + }); + }; + return ( + resetGroupChildrenPagination(level)} + pageIndex={pageIndex[level] ?? DEFAULT_PAGE_INDEX} + pageSize={pageSize[level] ?? DEFAULT_PAGE_SIZE} + parentGroupingFilter={parentGroupingFilter} + renderChildComponent={rcc} + selectedGroup={selectedGroup} + setPageIndex={(newIndex: number) => setPageVar(newIndex, level, 'index')} + setPageSize={(newSize: number) => setPageVar(newSize, level, 'size')} + /> + ); + }, + [getGrouping, pageIndex, pageSize, props, selectedGroups, setPageVar] ); if (isEmpty(selectedPatterns)) { return null; } - return groupedAlerts; + return getLevel(0, selectedGroups[0]); }; export const GroupedAlertsTable = React.memo(GroupedAlertsTableComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx new file mode 100644 index 00000000000000..cf8a8128cb166f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx @@ -0,0 +1,259 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import type { Filter, Query } from '@kbn/es-query'; +import { buildEsQuery } from '@kbn/es-query'; +import type { GroupingAggregation } from '@kbn/securitysolution-grouping'; +import { isNoneGroup } from '@kbn/securitysolution-grouping'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import type { DynamicGroupingProps } from '@kbn/securitysolution-grouping/src'; +import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; +import { combineQueries } from '../../../common/lib/kuery'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import type { AlertsGroupingAggregation } from './grouping_settings/types'; +import type { Status } from '../../../../common/detection_engine/schemas/common'; +import { InspectButton } from '../../../common/components/inspect'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import { useKibana } from '../../../common/lib/kibana'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; +import { useInspectButton } from '../alerts_kpis/common/hooks'; +import { buildTimeRangeFilter } from './helpers'; + +import * as i18n from './translations'; +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; +import { getAlertsGroupingQuery, useGroupTakeActionsItems } from './grouping_settings'; + +const ALERTS_GROUPING_ID = 'alerts-grouping'; + +interface OwnProps { + currentAlertStatusFilterValue?: Status[]; + defaultFilters?: Filter[]; + from: string; + getGrouping: ( + props: Omit, 'groupSelector' | 'pagination'> + ) => React.ReactElement; + globalFilters: Filter[]; + globalQuery: Query; + groupingLevel?: number; + hasIndexMaintenance: boolean; + hasIndexWrite: boolean; + loading: boolean; + onGroupClose: () => void; + pageIndex: number; + pageSize: number; + parentGroupingFilter?: string; + renderChildComponent: (groupingFilters: Filter[]) => React.ReactElement; + runtimeMappings: MappingRuntimeFields; + selectedGroup: string; + setPageIndex: (newIndex: number) => void; + setPageSize: (newSize: number) => void; + signalIndexName: string | null; + tableId: TableIdLiteral; + to: string; +} + +export type AlertsTableComponentProps = OwnProps; + +export const GroupedSubLevelComponent: React.FC = ({ + currentAlertStatusFilterValue, + defaultFilters = [], + from, + getGrouping, + globalFilters, + globalQuery, + groupingLevel, + hasIndexMaintenance, + hasIndexWrite, + loading, + onGroupClose, + pageIndex, + pageSize, + parentGroupingFilter, + renderChildComponent, + runtimeMappings, + selectedGroup, + setPageIndex, + setPageSize, + signalIndexName, + tableId, + to, +}) => { + const { + services: { uiSettings }, + } = useKibana(); + const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.detections); + + const getGlobalQuery = useCallback( + (customFilters: Filter[]) => { + if (browserFields != null && indexPattern != null) { + return combineQueries({ + config: getEsQueryConfig(uiSettings), + dataProviders: [], + indexPattern, + browserFields, + filters: [ + ...(defaultFilters ?? []), + ...globalFilters, + ...customFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ...buildTimeRangeFilter(from, to), + ], + kqlQuery: globalQuery, + kqlMode: globalQuery.language, + }); + } + return null; + }, + [ + browserFields, + defaultFilters, + from, + globalFilters, + globalQuery, + indexPattern, + parentGroupingFilter, + to, + uiSettings, + ] + ); + + const additionalFilters = useMemo(() => { + try { + return [ + buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [ + ...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []), + ...(defaultFilters ?? []), + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]), + ]; + } catch (e) { + return []; + } + }, [defaultFilters, globalFilters, globalQuery, parentGroupingFilter]); + + const queryGroups = useMemo(() => { + return getAlertsGroupingQuery({ + additionalFilters, + selectedGroup, + from, + runtimeMappings, + to, + pageSize, + pageIndex, + }); + }, [additionalFilters, from, pageIndex, pageSize, runtimeMappings, selectedGroup, to]); + + const emptyGlobalQuery = useMemo(() => getGlobalQuery([]), [getGlobalQuery]); + + useInvalidFilterQuery({ + id: tableId, + filterQuery: emptyGlobalQuery?.filterQuery, + kqlError: emptyGlobalQuery?.kqlError, + query: globalQuery, + startDate: from, + endDate: to, + }); + + const { + data: alertsGroupsData, + loading: isLoadingGroups, + refetch, + request, + response, + setQuery: setAlertsQuery, + } = useQueryAlerts<{}, GroupingAggregation>({ + query: queryGroups, + indexName: signalIndexName, + queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING, + skip: isNoneGroup([selectedGroup]), + }); + + useEffect(() => { + if (!isNoneGroup([selectedGroup])) { + setAlertsQuery(queryGroups); + } + }, [queryGroups, selectedGroup, setAlertsQuery]); + + const { deleteQuery, setQuery } = useGlobalTime(); + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []); + + useInspectButton({ + deleteQuery, + loading: isLoadingGroups, + refetch, + request, + response, + setQuery, + uniqueQueryId, + }); + + const inspect = useMemo( + () => ( + + ), + [uniqueQueryId] + ); + + const takeActionItems = useGroupTakeActionsItems({ + indexName: indexPattern.title, + currentStatus: currentAlertStatusFilterValue, + showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, + }); + + const getTakeActionItems = useCallback( + (groupFilters: Filter[], groupNumber: number) => + takeActionItems({ + groupNumber, + query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, + selectedGroup, + tableId, + }), + [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] + ); + + return useMemo( + () => + getGrouping({ + activePage: pageIndex, + data: alertsGroupsData?.aggregations, + groupingLevel, + inspectButton: inspect, + isLoading: loading || isLoadingGroups, + itemsPerPage: pageSize, + onChangeGroupsItemsPerPage: (size: number) => setPageSize(size), + onChangeGroupsPage: (index) => setPageIndex(index), + renderChildComponent, + onGroupClose, + selectedGroup, + takeActionItems: getTakeActionItems, + }), + [ + alertsGroupsData?.aggregations, + getGrouping, + getTakeActionItems, + groupingLevel, + inspect, + isLoadingGroups, + loading, + pageIndex, + pageSize, + renderChildComponent, + onGroupClose, + selectedGroup, + setPageIndex, + setPageSize, + ] + ); +}; + +export const GroupedSubLevel = React.memo(GroupedSubLevelComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx index f84305dcb3b379..d663b2abc2c611 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx @@ -30,7 +30,7 @@ describe('useGroupTakeActionsItems', () => { groupNumber: 0, selectedGroup: 'test', }; - it('returns array take actions items available for alerts table if showAlertStatusActions is true', async () => { + it('returns all take actions items if showAlertStatusActions is true and currentStatus is undefined', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -47,7 +47,106 @@ describe('useGroupTakeActionsItems', () => { }); }); - it('returns empty array of take actions items available for alerts table if showAlertStatusActions is false', async () => { + it('returns all take actions items if currentStatus is []', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: [], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); + + it('returns all take actions items if currentStatus.length > 1', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['open', 'closed'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); + + it('returns acknowledged & closed take actions items if currentStatus === ["open"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['open'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('acknowledge'); + expect(currentParams[1].key).toEqual('close'); + }); + }); + + it('returns open & acknowledged take actions items if currentStatus === ["closed"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['closed'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('open'); + expect(currentParams[1].key).toEqual('acknowledge'); + }); + }); + + it('returns open & closed take actions items if currentStatus === ["acknowledged"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['acknowledged'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('open'); + expect(currentParams[1].key).toEqual('close'); + }); + }); + + it('returns empty take actions items if showAlertStatusActions is false', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -63,4 +162,20 @@ describe('useGroupTakeActionsItems', () => { expect(result.current(getActionItemsParams).length).toEqual(0); }); }); + it('returns array take actions items if showAlertStatusActions is true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx index d2baadb99d1249..5d151d2e4cc886 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { Status } from '../../../../../common/detection_engine/schemas/common'; @@ -30,8 +30,9 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import * as i18n from '../translations'; import { getTelemetryEvent, METRIC_TYPE, track } from '../../../../common/lib/telemetry'; import type { StartServices } from '../../../../types'; + export interface TakeActionsProps { - currentStatus?: Status; + currentStatus?: Status[]; indexName: string; showAlertStatusActions?: boolean; } @@ -182,7 +183,7 @@ export const useGroupTakeActionsItems = ({ ] ); - const items = useMemo(() => { + return useMemo(() => { const getActionItems = ({ query, tableId, @@ -196,61 +197,89 @@ export const useGroupTakeActionsItems = ({ }) => { const actionItems: JSX.Element[] = []; if (showAlertStatusActions) { - if (currentStatus !== FILTER_OPEN) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_OPEN as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_OPEN_SELECTED} - - ); - } - if (currentStatus !== FILTER_ACKNOWLEDGED) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_ACKNOWLEDGED_SELECTED} - - ); - } - if (currentStatus !== FILTER_CLOSED) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_CLOSED as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_CLOSE_SELECTED} - + if (currentStatus && currentStatus.length === 1) { + const singleStatus = currentStatus[0]; + if (singleStatus !== FILTER_OPEN) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_OPEN as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_OPEN_SELECTED} + + ); + } + if (singleStatus !== FILTER_ACKNOWLEDGED) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_ACKNOWLEDGED_SELECTED} + + ); + } + if (singleStatus !== FILTER_CLOSED) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_CLOSED as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_CLOSE_SELECTED} + + ); + } + } else { + const statusArr = { + [FILTER_OPEN]: BULK_ACTION_OPEN_SELECTED, + [FILTER_ACKNOWLEDGED]: BULK_ACTION_ACKNOWLEDGED_SELECTED, + [FILTER_CLOSED]: BULK_ACTION_CLOSE_SELECTED, + }; + Object.keys(statusArr).forEach((workflowStatus) => + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: workflowStatus as AlertWorkflowStatus, + tableId, + }) + } + > + {statusArr[workflowStatus]} + + ) ); } } @@ -259,6 +288,4 @@ export const useGroupTakeActionsItems = ({ return getActionItems; }, [currentStatus, onClickUpdate, showAlertStatusActions]); - - return items; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts new file mode 100644 index 00000000000000..9e0b4e63715aa9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts @@ -0,0 +1,1736 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockAlertSearchResponse } from '../../../../common/components/alerts_treemap/lib/mocks/mock_alert_search_response'; + +export const groupingSearchResponse = { + ruleName: { + ...mockAlertSearchResponse, + hits: { + total: { + value: 6048, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + groupsCount: { + value: 32, + }, + groupByFields: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: ['critical hosts [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical users [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['critical users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['high users [Duplicate]', 'f'], + key_as_string: 'high users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['high users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['low users [Duplicate]', 'f'], + key_as_string: 'low users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['low users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['medium users [Duplicate]', 'f'], + key_as_string: 'medium users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['medium users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['critical hosts', 'f'], + key_as_string: 'critical hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts', 'f'], + key_as_string: 'high hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts ', 'f'], + key_as_string: 'low hosts |f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts', 'f'], + key_as_string: 'medium hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical users [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 91, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 91, + }, + { + key: 'rule', + doc_count: 91, + }, + ], + }, + unitsCount: { + value: 91, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 91, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + ], + }, + unitsCount: { + value: 6048, + }, + }, + }, + hostName: { + ...mockAlertSearchResponse, + hits: { + total: { + value: 900, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + groupsCount: { + value: 40, + }, + groupByFields: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'Host-f0m6ngo8fo', + doc_count: 75, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 75, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 75, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 25, + }, + }, + { + key: 'Host-4aijlqggv8', + doc_count: 63, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 63, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 63, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 21, + }, + }, + { + key: 'Host-e50lhbdm91', + doc_count: 51, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 51, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 51, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 17, + }, + }, + { + key: 'sqp', + doc_count: 42, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 42, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 42, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'sUl', + doc_count: 33, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 33, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 33, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'vLJ', + doc_count: 30, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 30, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 30, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Host-n28uwmsqmd', + doc_count: 27, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 27, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 27, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 9, + }, + }, + { + key: 'JaE', + doc_count: 27, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 27, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 27, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'CUA', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'FWT', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'ZqT', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'mmn', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'xRS', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'HiC', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Host-d7zbfvl3zz', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 7, + }, + }, + { + key: 'Nnc', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'OqH', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Vaw', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'XPg', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'qBS', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'rwt', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'xVJ', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Bxg', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'efP', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'qcb', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + ], + }, + unitsCount: { + value: 900, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts index 624b343c14cf95..921a8d3e3d43f1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts @@ -42,8 +42,8 @@ export const getAlertsGroupingQuery = ({ getGroupingQuery({ additionalFilters, from, - groupByFields: !isNoneGroup(selectedGroup) ? getGroupFields(selectedGroup) : [], - statsAggregations: !isNoneGroup(selectedGroup) + groupByFields: !isNoneGroup([selectedGroup]) ? getGroupFields(selectedGroup) : [], + statsAggregations: !isNoneGroup([selectedGroup]) ? getAggregationsByGroupField(selectedGroup) : [], pageNumber: pageIndex * pageSize, @@ -51,7 +51,7 @@ export const getAlertsGroupingQuery = ({ { unitsCount: { value_count: { field: selectedGroup } }, }, - ...(!isNoneGroup(selectedGroup) + ...(!isNoneGroup([selectedGroup]) ? [{ groupsCount: { cardinality: { field: selectedGroup } } }] : []), ], diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx deleted file mode 100644 index 346e4b51df72d9..00000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ /dev/null @@ -1,261 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import type { Filter } from '@kbn/es-query'; -import useResizeObserver from 'use-resize-observer/polyfilled'; - -import '../../../common/mock/match_media'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, - TestProviders, -} from '../../../common/mock'; -import type { AlertsTableComponentProps } from './alerts_grouping'; -import { GroupedAlertsTableComponent } from './alerts_grouping'; -import { TableId } from '@kbn/securitysolution-data-table'; -import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; -import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; -import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; -import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; -import type { State } from '../../../common/store'; -import { createStore } from '../../../common/store'; -import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; - -jest.mock('@kbn/securitysolution-grouping'); - -jest.mock('../../../common/containers/sourcerer'); -jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest.fn().mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: jest.fn(), - }), -})); - -jest.mock('./grouping_settings', () => ({ - getAlertsGroupingQuery: jest.fn(), - getDefaultGroupingOptions: () => [ - { label: 'ruleName', key: 'kibana.alert.rule.name' }, - { label: 'userName', key: 'user.name' }, - { label: 'hostName', key: 'host.name' }, - { label: 'sourceIP', key: 'source.ip' }, - ], - getSelectedGroupBadgeMetrics: jest.fn(), - getSelectedGroupButtonContent: jest.fn(), - getSelectedGroupCustomMetrics: jest.fn(), - useGroupTakeActionsItems: jest.fn(), -})); - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); -jest.mock('../../../common/utils/normalize_time_range'); - -const mockUseFieldBrowserOptions = jest.fn(); -jest.mock('../../../timelines/components/fields_browser', () => ({ - useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), -})); - -const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; -jest.mock('use-resize-observer/polyfilled'); -mockUseResizeObserver.mockImplementation(() => ({})); - -const mockFilterManager = createFilterManagerMock(); - -const mockKibanaServices = createStartServicesMock(); - -jest.mock('../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../common/lib/kibana'); - - return { - ...original, - useUiSetting$: jest.fn().mockReturnValue([]), - useKibana: () => ({ - services: { - ...mockKibanaServices, - application: { - navigateToUrl: jest.fn(), - capabilities: { - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - cases: { - ui: { getCasesContext: mockCasesContext }, - }, - uiSettings: { - get: jest.fn(), - }, - timelines: { ...mockTimelines }, - data: { - query: { - filterManager: mockFilterManager, - }, - }, - docLinks: { - links: { - siem: { - privileges: 'link', - }, - }, - }, - storage: { - get: jest.fn(), - set: jest.fn(), - }, - triggerActionsUi: { - getAlertsStateTable: jest.fn(() => <>), - alertsTableConfigurationRegistry: {}, - }, - }, - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - remove: jest.fn(), - }), - }; -}); -const state: State = { - ...mockGlobalState, -}; -const { storage } = createSecuritySolutionStorageMock(); -const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - -const groupingStore = createStore( - { - ...state, - groups: { - groupSelector: <>, - selectedGroup: 'host.name', - }, - }, - SUB_PLUGINS_REDUCER, - kibanaObservable, - storage -); - -jest.mock('./timeline_actions/use_add_bulk_to_timeline', () => ({ - useAddBulkToTimelineAction: jest.fn(() => {}), -})); - -const sourcererDataView = { - indicesExist: true, - loading: false, - indexPattern: { - fields: [], - }, - browserFields: {}, -}; -const renderChildComponent = (groupingFilters: Filter[]) =>

; - -const testProps: AlertsTableComponentProps = { - defaultFilters: [], - from: '2020-07-07T08:20:18.966Z', - globalFilters: [], - globalQuery: { - query: 'query', - language: 'language', - }, - hasIndexMaintenance: true, - hasIndexWrite: true, - loading: false, - renderChildComponent, - runtimeMappings: {}, - signalIndexName: 'test', - tableId: TableId.test, - to: '2020-07-08T08:20:18.966Z', -}; - -const resetPagination = jest.fn(); - -describe('GroupedAlertsTable', () => { - const getGrouping = jest.fn().mockReturnValue(); - beforeEach(() => { - jest.clearAllMocks(); - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - selectedPatterns: ['myFakebeat-*'], - }); - (isNoneGroup as jest.Mock).mockReturnValue(true); - (useGrouping as jest.Mock).mockReturnValue({ - groupSelector: <>, - getGrouping, - selectedGroup: 'host.name', - pagination: { pageSize: 1, pageIndex: 0, reset: resetPagination }, - }); - }); - - it('calls the proper initial dispatch actions for groups', () => { - render( - - - - ); - expect(mockDispatch).toHaveBeenCalledTimes(2); - expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR' - ); - expect(mockDispatch.mock.calls[1][0].type).toEqual( - 'x-pack/security_solution/groups/UPDATE_SELECTED_GROUP' - ); - }); - - it('renders grouping table', async () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - - const { getByTestId } = render( - - - - ); - expect(getByTestId('grouping-table')).toBeInTheDocument(); - expect(getGrouping.mock.calls[0][0].isLoading).toEqual(false); - }); - - it('renders loading when expected', () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - render( - - - - ); - expect(getGrouping.mock.calls[0][0].isLoading).toEqual(true); - }); - - it('resets grouping pagination when global query updates', () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - const { rerender } = render( - - - - ); - // called on initial query definition - expect(resetPagination).toHaveBeenCalledTimes(1); - rerender( - - - - ); - expect(resetPagination).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx index 78d736e99c93ea..92f77c3e2df917 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { isNoneGroup } from '@kbn/securitysolution-grouping'; import { dataTableSelectors, tableDefaults, @@ -29,9 +28,6 @@ export const getPersistentControlsHook = (tableId: TableId) => { const getGroupSelector = groupSelectors.getGroupSelector(); const groupSelector = useSelector((state: State) => getGroupSelector(state)); - const getSelectedGroup = groupSelectors.getSelectedGroup(); - - const selectedGroup = useSelector((state: State) => getSelectedGroup(state)); const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); @@ -88,10 +84,10 @@ export const getPersistentControlsHook = (tableId: TableId) => { hasRightOffset={false} additionalFilters={additionalFiltersComponent} showInspect={false} - additionalMenuOptions={isNoneGroup(selectedGroup) ? [groupSelector] : []} + additionalMenuOptions={groupSelector != null ? [groupSelector] : []} /> ), - [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector, selectedGroup] + [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector] ); return { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 64e489599dd05c..13836e658eee72 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React from 'react'; -import { mount } from 'enzyme'; +import React, { useEffect } from 'react'; +import { render, waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; -import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { createSecuritySolutionStorageMock, @@ -29,6 +28,10 @@ import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_cont import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { useListsConfig } from '../../containers/detection_engine/lists/use_lists_config'; +import type { FilterGroupProps } from '../../../common/components/filter_group/types'; +import { FilterGroup } from '../../../common/components/filter_group'; +import type { AlertsTableComponentProps } from '../../components/alerts_table/alerts_grouping'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -38,6 +41,27 @@ jest.mock('../../../common/components/search_bar', () => ({ jest.mock('../../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../../common/hooks/use_space_id', () => ({ + useSpaceId: () => 'default', +})); +jest.mock('../../../common/components/filter_group'); + +const mockStatusCapture = jest.fn(); +const GroupedAlertsTable: React.FC = ({ + currentAlertStatusFilterValue, +}) => { + useEffect(() => { + if (currentAlertStatusFilterValue) { + mockStatusCapture(currentAlertStatusFilterValue); + } + }, [currentAlertStatusFilterValue]); + return ; +}; + +jest.mock('../../components/alerts_table/alerts_grouping', () => ({ + GroupedAlertsTable, +})); + jest.mock('../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../components/user_info'); jest.mock('../../../common/containers/sourcerer'); @@ -158,9 +182,11 @@ jest.mock('../../../common/components/page/use_refetch_by_session'); describe('DetectionEnginePageComponent', () => { beforeAll(() => { + (useListsConfig as jest.Mock).mockReturnValue({ loading: false, needsConfiguration: false }); (useParams as jest.Mock).mockReturnValue({}); (useUserData as jest.Mock).mockReturnValue([ { + loading: false, hasIndexRead: true, canUserREAD: true, }, @@ -170,10 +196,15 @@ describe('DetectionEnginePageComponent', () => { indexPattern: {}, browserFields: mockBrowserFields, }); + (FilterGroup as jest.Mock).mockImplementation(() => { + return ; + }); + }); + beforeEach(() => { + jest.clearAllMocks(); }); - it('renders correctly', async () => { - const wrapper = mount( + const { getByTestId } = render( @@ -181,12 +212,12 @@ describe('DetectionEnginePageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('FiltersGlobal').exists()).toBe(true); + expect(getByTestId('filter-group__loading')).toBeInTheDocument(); }); }); it('renders the chart panels', async () => { - const wrapper = mount( + const { getByTestId } = render( @@ -195,7 +226,119 @@ describe('DetectionEnginePageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('[data-test-subj="chartPanels"]').exists()).toBe(true); + expect(getByTestId('chartPanels')).toBeInTheDocument(); + }); + }); + + it('the pageFiltersUpdateHandler updates status when a multi status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.workflow_status', + params: ['open', 'acknowledged'], + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); + }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open', 'acknowledged']); + }); + + it('the pageFiltersUpdateHandler updates status when a single status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.workflow_status', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.severity', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.severity': 'low', + }, + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); + }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open']); + }); + + it('the pageFiltersUpdateHandler clears status when no status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.severity', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.severity': 'low', + }, + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, []); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 0eacecd46e8fcf..1ccfaea4584ee4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -29,7 +29,6 @@ import { dataTableActions, dataTableSelectors, tableDefaults, - FILTER_OPEN, TableId, } from '@kbn/securitysolution-data-table'; import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../common/constants'; @@ -139,7 +138,7 @@ const DetectionEnginePageComponent: React.FC = ({ const arePageFiltersEnabled = useIsExperimentalFeatureEnabled('alertsPageFiltersEnabled'); // when arePageFiltersEnabled === false - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const [statusFilter, setStatusFilter] = useState([]); const updatedAt = useShallowEqualSelector( (state) => (getTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults).updated @@ -177,8 +176,8 @@ const DetectionEnginePageComponent: React.FC = ({ if (arePageFiltersEnabled) { return detectionPageFilters; } - return buildAlertStatusFilter(filterGroup); - }, [filterGroup, detectionPageFilters, arePageFiltersEnabled]); + return buildAlertStatusFilter(statusFilter[0] ?? 'open'); + }, [statusFilter, detectionPageFilters, arePageFiltersEnabled]); useEffect(() => { if (!detectionPageFilterHandler) return; @@ -276,6 +275,19 @@ const DetectionEnginePageComponent: React.FC = ({ const pageFiltersUpdateHandler = useCallback((newFilters: Filter[]) => { setDetectionPageFilters(newFilters); + if (newFilters.length) { + const newStatusFilter = newFilters.find( + (filter) => filter.meta.key === 'kibana.alert.workflow_status' + ); + if (newStatusFilter) { + const status: Status[] = newStatusFilter.meta.params + ? (newStatusFilter.meta.params as Status[]) + : [newStatusFilter.query?.match_phrase['kibana.alert.workflow_status']]; + setStatusFilter(status); + } else { + setStatusFilter([]); + } + } }, []); // Callback for when open/closed filter changes @@ -284,9 +296,9 @@ const DetectionEnginePageComponent: React.FC = ({ const timelineId = TableId.alertsOnAlertsPage; clearEventsLoading({ id: timelineId }); clearEventsDeleted({ id: timelineId }); - setFilterGroup(newFilterGroup); + setStatusFilter([newFilterGroup]); }, - [clearEventsLoading, clearEventsDeleted, setFilterGroup] + [clearEventsLoading, clearEventsDeleted, setStatusFilter] ); const areDetectionPageFiltersLoading = useMemo(() => { @@ -317,7 +329,7 @@ const DetectionEnginePageComponent: React.FC = ({ @@ -352,7 +364,7 @@ const DetectionEnginePageComponent: React.FC = ({ [ arePageFiltersEnabled, dataViewId, - filterGroup, + statusFilter, filters, onFilterGroupChangedCallback, pageFiltersUpdateHandler, @@ -462,7 +474,7 @@ const DetectionEnginePageComponent: React.FC = ({ { const [updatedAt, setUpdatedAt] = useState(Date.now()); const { toggleStatus, setToggleStatus } = useQueryToggle(TABLE_QUERY_ID); - const { deleteQuery, setQuery, from, to } = useGlobalTime(false); + const { deleteQuery, setQuery, from, to } = useGlobalTime(); const { isLoading: isSearchLoading, data, diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx index 28e5c696d1f52c..85cc8c00438970 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx @@ -41,7 +41,7 @@ const HOST_RISK_QUERY_ID = 'hostRiskScoreKpiQuery'; const USER_RISK_QUERY_ID = 'userRiskScoreKpiQuery'; export const EntityAnalyticsHeader = () => { - const { from, to } = useGlobalTime(false); + const { from, to } = useGlobalTime(); const timerange = useMemo( () => ({ from, From b5c88d90ceac26e990dcde8c5e8df524cad0a2a8 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 24 Apr 2023 09:07:15 -0400 Subject: [PATCH 43/65] [Security Solution] Move policy meta updates to policy update (#155462) ## Summary Moving the new meta fields in Policy to update when the Policy update callback is called. These fields are used in telemetry. These fields are being moved from the Policy watcher to avoid triggering a policy deploy on many Agents at once on upgrade. Instead, these fields will be updated whenever the next Endpoint policy update comes. New Policies will have the telemetry fields already populated. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../endpoint/endpoint_app_context_services.ts | 3 +- .../endpoint/lib/policy/license_watch.test.ts | 26 +------ .../endpoint/lib/policy/license_watch.ts | 7 -- .../fleet_integration.test.ts | 69 ++++++++++++++++++- .../fleet_integration/fleet_integration.ts | 30 +++++++- .../security_solution/server/plugin.ts | 1 - 6 files changed, 100 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 489953a2d96d29..8e1855b2cd84cd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -125,7 +125,8 @@ export class EndpointAppContextService { logger, licenseService, featureUsageService, - endpointMetadataService + endpointMetadataService, + cloud ) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts index 9c57b8156e3e3e..9d962bc0e64ced 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts @@ -16,7 +16,6 @@ import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks'; import { PolicyWatcher } from './license_watch'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; -import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; @@ -36,7 +35,6 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa describe('Policy-Changing license watcher', () => { const logger = loggingSystemMock.create().get('license_watch.test'); - const cloudServiceMock = cloudMock.createSetup(); const soStartMock = savedObjectsServiceMock.createStartContract(); const esStartMock = elasticsearchServiceMock.createStart(); let packagePolicySvcMock: jest.Mocked; @@ -53,13 +51,7 @@ describe('Policy-Changing license watcher', () => { // mock a license-changing service to test reactivity const licenseEmitter: Subject = new Subject(); const licenseService = new LicenseService(); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // swap out watch function, just to ensure it gets called when a license change happens const mockWatch = jest.fn(); @@ -104,13 +96,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); await pw.watch(Gold); // just manually trigger with a given license expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts @@ -137,13 +123,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // emulate a license change below paid tier await pw.watch(Basic); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index dded85b559c6d2..195c8509e60d5e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -17,7 +17,6 @@ import type { } from '@kbn/core/server'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; -import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; @@ -35,19 +34,16 @@ export class PolicyWatcher { private policyService: PackagePolicyClient; private subscription: Subscription | undefined; private soStart: SavedObjectsServiceStart; - private cloud: CloudSetup; constructor( policyService: PackagePolicyClient, soStart: SavedObjectsServiceStart, esStart: ElasticsearchServiceStart, - cloud: CloudSetup, logger: Logger ) { this.policyService = policyService; this.esClient = esStart.client.asInternalUser; this.logger = logger; this.soStart = soStart; - this.cloud = cloud; } /** @@ -105,9 +101,6 @@ export class PolicyWatcher { for (const policy of response.items as PolicyData[]) { const updatePolicy = getPolicyDataForUpdate(policy); const policyConfig = updatePolicy.inputs[0].config.policy.value; - updatePolicy.inputs[0].config.policy.value.meta.license = license.type || ''; - // add cloud info to policy meta - updatePolicy.inputs[0].config.policy.value.meta.cloud = this.cloud?.isCloudEnabled; try { if (!isEndpointPolicyValidForLicense(policyConfig, license)) { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 30c7e776e718e9..c415fc287fbecf 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -366,7 +366,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -382,7 +383,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -412,7 +414,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -427,6 +430,66 @@ describe('ingest_integration tests ', () => { }); }); + describe('package policy update callback when meta fields should be updated', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + beforeEach(() => { + licenseEmitter.next(Platinum); // set license level to platinum + }); + it('updates successfully when meta fields differ from services', async () => { + const mockPolicy = policyFactory(); + mockPolicy.meta.cloud = true; // cloud mock will return true + mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyUpdateCallback( + logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService + ); + const policyConfig = generator.generatePolicyPackagePolicy(); + // values should be updated + policyConfig.inputs[0]!.config!.policy.value.meta.cloud = false; + policyConfig.inputs[0]!.config!.policy.value.meta.license = 'gold'; + const updatedPolicyConfig = await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); + }); + + it('meta fields stay the same where there is no difference', async () => { + const mockPolicy = policyFactory(); + mockPolicy.meta.cloud = true; // cloud mock will return true + mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyUpdateCallback( + logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService + ); + const policyConfig = generator.generatePolicyPackagePolicy(); + // values should be updated + policyConfig.inputs[0]!.config!.policy.value.meta.cloud = true; + policyConfig.inputs[0]!.config!.policy.value.meta.license = 'platinum'; + const updatedPolicyConfig = await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); + }); + }); + describe('package policy delete callback', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index fd6f85af1a0455..7e68c63f075938 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -44,6 +44,17 @@ const isEndpointPackagePolicy = ( return packagePolicy.package?.name === 'endpoint'; }; +const shouldUpdateMetaValues = ( + endpointPackagePolicy: PolicyConfig, + currentLicenseType: string, + currentCloudInfo: boolean +) => { + return ( + endpointPackagePolicy.meta.license !== currentLicenseType || + endpointPackagePolicy.meta.cloud !== currentCloudInfo + ); +}; + /** * Callback to handle creation of PackagePolicies in Fleet */ @@ -152,7 +163,8 @@ export const getPackagePolicyUpdateCallback = ( logger: Logger, licenseService: LicenseService, featureUsageService: FeatureUsageService, - endpointMetadataService: EndpointMetadataService + endpointMetadataService: EndpointMetadataService, + cloud: CloudSetup ): PutPackagePolicyUpdateCallback => { return async (newPackagePolicy: NewPackagePolicy): Promise => { if (!isEndpointPackagePolicy(newPackagePolicy)) { @@ -170,6 +182,22 @@ export const getPackagePolicyUpdateCallback = ( notifyProtectionFeatureUsage(newPackagePolicy, featureUsageService, endpointMetadataService); + const newEndpointPackagePolicy = newPackagePolicy.inputs[0].config?.policy + ?.value as PolicyConfig; + + if ( + newPackagePolicy.inputs[0].config?.policy?.value && + shouldUpdateMetaValues( + newEndpointPackagePolicy, + licenseService.getLicenseType(), + cloud?.isCloudEnabled + ) + ) { + newEndpointPackagePolicy.meta.license = licenseService.getLicenseType(); + newEndpointPackagePolicy.meta.cloud = cloud?.isCloudEnabled; + newPackagePolicy.inputs[0].config.policy.value = newEndpointPackagePolicy; + } + return newPackagePolicy; }; }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ab5ac00454a5eb..e6ad26f1f405f6 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -472,7 +472,6 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.fleet.packagePolicyService, core.savedObjects, core.elasticsearch, - plugins.cloud, logger ); this.policyWatcher.start(licenseService); From b71f7831d80785df4d07831388d5d082d31ec8d6 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Mon, 24 Apr 2023 15:22:56 +0200 Subject: [PATCH 44/65] [AO] Sync chart pointers on the metric threshold alert details page (#155402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #155354 ## Summary This PR syncs chart pointers on the metric threshold alert details page. ![image](https://user-images.githubusercontent.com/12370520/233380782-bf97eab8-167d-4f97-a10d-d2bfec1936e7.png) ## 🧪 How to test - Add `xpack.observability.unsafe.alertDetails.metrics.enabled: true` to the Kibana config - Create a metric threshold rule with multiple conditions that generates an alert - Go to the alert details page and check the chart pointers --- .../components/expression_chart.test.tsx | 3 +++ .../components/expression_chart.tsx | 21 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index f2bb22485fe9c0..745a1f0169788a 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -22,6 +22,9 @@ jest.mock('../../../hooks/use_kibana', () => ({ useKibanaContextForPlugin: () => ({ services: { ...mockStartServices, + charts: { + activeCursor: jest.fn(), + }, }, }), })); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 8dd7762feb6b99..8b453579b5e678 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useRef } from 'react'; import { Axis, Chart, @@ -17,6 +17,7 @@ import { } from '@elastic/charts'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useActiveCursor } from '@kbn/charts-plugin/public'; import { DataViewBase } from '@kbn/es-query'; import { first, last } from 'lodash'; @@ -66,7 +67,7 @@ export const ExpressionChart: React.FC = ({ timeRange, annotations, }) => { - const { uiSettings } = useKibanaContextForPlugin().services; + const { uiSettings, charts } = useKibanaContextForPlugin().services; const { isLoading, data } = useMetricsExplorerChartData( expression, @@ -77,6 +78,11 @@ export const ExpressionChart: React.FC = ({ timeRange ); + const chartRef = useRef(null); + const handleCursorUpdate = useActiveCursor(charts.activeCursor, chartRef, { + isDateHistogram: true, + }); + if (isLoading) { return ; } @@ -141,7 +147,7 @@ export const ExpressionChart: React.FC = ({ return ( <> - + = ({ tickFormat={createFormatterForMetric(metric)} domain={domain} /> - +

From 4dc21e5589067fe68ca2767204759e8579914c16 Mon Sep 17 00:00:00 2001 From: Apoorva Joshi <30438249+ajosh0504@users.noreply.github.com> Date: Mon, 24 Apr 2023 06:51:06 -0700 Subject: [PATCH 45/65] Updates to pre-built Security ML jobs (#154596) ## Summary This PR makes the following updates to the pre-built Security ML jobs: - Making the `security-packetbeat` compatible with Agent - Removing superfluous fields from the job configurations to make them consistent - Updating the `detector_description` field for almost all jobs - Adding influencers where missing and/or relevant - Adding a `job_revision` custom setting similar to the Logs [jobs](https://github.com/elastic/kibana/blob/main/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json#L29). Moving forward, this number will be updated each time a job is updated. We are starting with 4 since the `linux` and `windows` jobs are at v3 right now - Adding a `managed`: `true` tag to indicate that these jobs are pre-configured by Elastic and so users will see the warnings added in [this](https://github.com/elastic/kibana/pull/122305) PR if users choose to delete, or modify these jobs --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../modules/security_auth/manifest.json | 4 +- .../ml/auth_high_count_logon_events.json | 14 ++--- ...gh_count_logon_events_for_a_source_ip.json | 18 ++---- .../ml/auth_high_count_logon_fails.json | 14 ++--- .../ml/auth_rare_hour_for_a_user.json | 18 +++--- .../ml/auth_rare_source_ip_for_a_user.json | 18 +++--- .../security_auth/ml/auth_rare_user.json | 18 +++--- .../datafeed_suspicious_login_activity.json | 9 +-- .../ml/suspicious_login_activity.json | 27 +++------ .../modules/security_cloudtrail/manifest.json | 12 ++-- .../ml/high_distinct_count_error_message.json | 21 +++---- .../ml/rare_error_code.json | 20 +++---- .../ml/rare_method_for_a_city.json | 20 +++---- .../ml/rare_method_for_a_country.json | 20 +++---- .../ml/rare_method_for_a_username.json | 17 +++--- .../modules/security_linux/manifest.json | 7 +-- .../v3_linux_anomalous_network_activity.json | 49 +++-------------- ...linux_anomalous_network_port_activity.json | 49 +++-------------- .../v3_linux_anomalous_process_all_hosts.json | 49 +++-------------- .../ml/v3_linux_anomalous_user_name.json | 48 +++------------- ...linux_network_configuration_discovery.json | 51 +++-------------- ...v3_linux_network_connection_discovery.json | 51 +++-------------- .../ml/v3_linux_rare_metadata_process.json | 30 +++------- .../ml/v3_linux_rare_metadata_user.json | 29 +++------- .../ml/v3_linux_rare_sudo_user.json | 51 +++-------------- .../ml/v3_linux_rare_user_compiler.json | 43 +++------------ ...v3_linux_system_information_discovery.json | 51 +++-------------- .../ml/v3_linux_system_process_discovery.json | 51 +++-------------- .../ml/v3_linux_system_user_discovery.json | 49 +++-------------- .../ml/v3_rare_process_by_host_linux.json | 48 +++------------- .../modules/security_network/manifest.json | 4 +- .../ml/high_count_by_destination_country.json | 14 ++--- .../ml/high_count_network_denies.json | 14 ++--- .../ml/high_count_network_events.json | 14 ++--- .../ml/rare_destination_country.json | 11 ++-- .../modules/security_packetbeat/manifest.json | 10 ++-- .../ml/datafeed_packetbeat_dns_tunneling.json | 16 +++--- ...datafeed_packetbeat_rare_dns_question.json | 16 +++--- .../datafeed_packetbeat_rare_user_agent.json | 16 +++--- .../ml/packetbeat_dns_tunneling.json | 29 +++------- .../ml/packetbeat_rare_dns_question.json | 22 ++------ .../ml/packetbeat_rare_server_domain.json | 24 ++------ .../ml/packetbeat_rare_urls.json | 23 ++------ .../ml/packetbeat_rare_user_agent.json | 23 ++------ .../ml/v3_rare_process_by_host_windows.json | 53 +++--------------- ...v3_windows_anomalous_network_activity.json | 53 +++--------------- .../v3_windows_anomalous_path_activity.json | 52 +++--------------- ...3_windows_anomalous_process_all_hosts.json | 55 +++---------------- ...v3_windows_anomalous_process_creation.json | 53 +++--------------- .../ml/v3_windows_anomalous_script.json | 42 +++----------- .../ml/v3_windows_anomalous_service.json | 37 +++---------- .../ml/v3_windows_anomalous_user_name.json | 53 +++--------------- .../ml/v3_windows_rare_metadata_process.json | 34 +++--------- .../ml/v3_windows_rare_metadata_user.json | 33 +++-------- .../ml/v3_windows_rare_user_runas_event.json | 46 ++-------------- ...windows_rare_user_type10_remote_login.json | 46 ++-------------- 56 files changed, 386 insertions(+), 1313 deletions(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json index b3395d82a9c29b..d600e4a637acf0 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json @@ -2,7 +2,7 @@ "id": "security_auth", "title": "Security: Authentication", "description": "Detect anomalous activity in your ECS-compatible authentication logs.", - "type": "auth data", + "type": "Auth data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*,filebeat-*,winlogbeat-*", "query": { @@ -14,7 +14,7 @@ } } ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json index 7ca7a5ebd71e48..ac50e2f53535c6 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json @@ -1,20 +1,16 @@ { "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration, or brute force activity.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of logon events", + "detector_description": "Detects high count of logon events.", "function": "high_non_zero_count", "detector_index": 0 } ], - "influencers": [], - "model_prune_window": "30d" + "influencers": ["source.ip", "winlog.event_data.LogonType", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -25,6 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Logon Events" + "security_app_display_name": "Spike in Logon Events", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json index 47096f4c6413f2..d23f8df88ef6af 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json @@ -1,25 +1,17 @@ { "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events from a particular source IP address. This can be due to password spraying, user enumeration, or brute force activity.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of auth events for a source IP", + "detector_description": "Detects high count of auth events for a source IP.", "function": "high_non_zero_count", "by_field_name": "source.ip", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "winlog.event_data.LogonType", - "user.name" - ], - "model_prune_window": "30d" + "influencers": ["source.ip", "winlog.event_data.LogonType", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Logon Events from a Source IP" + "security_app_display_name": "Spike in Logon Events from a Source IP", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json index 48586ef642ca63..db2db5ea008324 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json @@ -1,20 +1,16 @@ { "description": "Security: Authentication - Looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration, or brute force activity and may be a precursor to account takeover or credentialed access.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of logon fails", + "detector_description": "Detects high count of logon fails.", "function": "high_non_zero_count", "detector_index": 0 } ], - "influencers": [], - "model_prune_window": "30d" + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -25,6 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Failed Logon Events" + "security_app_display_name": "Spike in Failed Logon Events", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json index 1f421ed298b9f8..57477497aeb623 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json @@ -1,23 +1,17 @@ { - "description": "Security: Authentication - looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare hour for a user", + "detector_description": "Detects rare hour for a user.", "function": "time_of_day", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Unusual Hour for a User to Logon" + "security_app_display_name": "Unusual Hour for a User to Logon", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json index 98a249074a67a1..81185ef5039c7e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json @@ -1,24 +1,18 @@ { - "description": "Security: Authentication - looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare source IP for a user", + "detector_description": "Detects rare source IP for a user.", "function": "rare", "by_field_name": "source.ip", "partition_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Unusual Source IP for a User to Logon from" + "security_app_display_name": "Unusual Source IP for a User to Logon from", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json index e2488480e61d19..58530fe0850145 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json @@ -1,23 +1,17 @@ { - "description": "Security: Authentication - looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare user", + "detector_description": "Detects rare user authentication.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Rare User Logon" + "security_app_display_name": "Rare User Logon", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json index 386b9fab256679..59a9129e7b7bf8 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json @@ -1,15 +1,10 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": { "event.category": "authentication" }}, - {"term": { "agent.type": "auditbeat" }} - ] + "filter": [{ "term": { "event.category": "authentication" } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json index 00e810b5348e7e..bbe420b3ec0ebc 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json @@ -1,24 +1,17 @@ { - "description": "Security: Auditbeat - Detect unusually high number of authentication attempts.", - "groups": [ - "security", - "auditbeat", - "authentication" - ], + "description": "Security: Authentication - Detects unusually high number of authentication attempts.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high number of authentication attempts", + "detector_description": "Detects high number of authentication attempts for a host.", "function": "high_non_zero_count", - "partition_field_name": "host.name" + "partition_field_name": "host.name", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "source.ip" - ], + "influencers": ["host.name", "user.name", "source.ip"], "model_prune_window": "30d" }, "allow_lazy_open": true, @@ -31,11 +24,7 @@ "custom_settings": { "created_by": "ml-module-security-auth", "security_app_display_name": "Unusual Login Activity", - "custom_urls": [ - { - "url_name": "IP Address Details", - "url_value": "security/network/ml-network/ip/$source.ip$?_g=()&query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ] + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json index 93797b9e3e758e..52b406a0da7cb4 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json @@ -1,16 +1,14 @@ { "id": "security_cloudtrail", "title": "Security: Cloudtrail", - "description": "Detect suspicious activity recorded in your cloudtrail logs.", - "type": "Filebeat data", + "description": "Detect suspicious activity recorded in Cloudtrail logs.", + "type": "Cloudtrail data", "logoFile": "logo.json", - "defaultIndexPattern": "filebeat-*", + "defaultIndexPattern": "logs-*,filebeat-*", "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "aws.cloudtrail"}} - ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "filter": [{ "term": { "event.dataset": "aws.cloudtrail" } }], + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json index 11b5f4625a4846..2ba7c4fdf4085d 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json @@ -1,24 +1,17 @@ { "description": "Security: Cloudtrail - Looks for a spike in the rate of an error message which may simply indicate an impending service failure but these can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_distinct_count(\"aws.cloudtrail.error_message\")", + "detector_description": "Detects high distinct count of Cloudtrail error messages.", "function": "high_distinct_count", - "field_name": "aws.cloudtrail.error_message" + "field_name": "aws.cloudtrail.error_message", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ], - "model_prune_window": "30d" + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Spike in AWS Error Messages" + "security_app_display_name": "Spike in AWS Error Messages", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json index c54c8e8378f2c7..7752430876e3f1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json @@ -1,23 +1,17 @@ { "description": "Security: Cloudtrail - Looks for unusual errors. Rare and unusual errors may simply indicate an impending service failure but they can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"aws.cloudtrail.error_code\"", + "detector_description": "Detects rare Cloudtrail error codes.", "function": "rare", - "by_field_name": "aws.cloudtrail.error_code" + "by_field_name": "aws.cloudtrail.error_code", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Rare AWS Error Code" + "security_app_display_name": "Rare AWS Error Code", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json index 2ed28884be94fc..f7be6fe8cc8d78 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json @@ -1,24 +1,18 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (city) that is unusual. This can be the result of compromised credentials or keys.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"source.geo.city_name\"", + "detector_description": "Detects rare event actions for a city.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "source.geo.city_name" + "partition_field_name": "source.geo.city_name", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual City for an AWS Command" + "security_app_display_name": "Unusual City for an AWS Command", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json index 1f14357e734445..d73f51f34de3ad 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json @@ -1,24 +1,18 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (country) that is unusual. This can be the result of compromised credentials or keys.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"source.geo.country_iso_code\"", + "detector_description": "Detects rare event actions for an ISO code.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "source.geo.country_iso_code" + "partition_field_name": "source.geo.country_iso_code", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.country_iso_code" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.country_iso_code"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual Country for an AWS Command" + "security_app_display_name": "Unusual Country for an AWS Command", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json index 76cce7fb829cad..a5080286198331 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json @@ -1,23 +1,22 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a user context that does not normally call the method. This can be the result of compromised credentials or keys as someone uses a valid account to persist, move laterally, or exfil data.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"user.name\"", + "detector_description": "Detects rare event actions for a user.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "user.name" + "partition_field_name": "user.name", + "detector_index": 0 } ], "influencers": [ "user.name", "source.ip", - "source.geo.city_name" + "source.geo.city_name", + "aws.cloudtrail.user_identity.arn" ] }, "allow_lazy_open": true, @@ -29,6 +28,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual AWS Command for a User" + "security_app_display_name": "Unusual AWS Command for a User", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json index 269f90dea4471e..cfff61e304c0ed 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json @@ -2,7 +2,7 @@ "id": "security_linux_v3", "title": "Security: Linux", "description": "Anomaly detection jobs for Linux host-based threat hunting and detection.", - "type": "linux data", + "type": "Linux data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*", "query": { @@ -43,10 +43,7 @@ ], "must_not": { "terms": { - "_tier": [ - "data_frozen", - "data_cold" - ] + "_tier": ["data_frozen", "data_cold"] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json index 29f6bf1d98412d..b276bcc7856bac 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "network", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", - "by_field_name": "process.name" + "by_field_name": "process.name", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4004", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Activity" + "security_app_display_name": "Unusual Linux Network Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json index 34b97358260acc..a551d6c2c204f7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "network" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare destination.port values.", + "detector_description": "Detects rare destination ports.", "function": "rare", - "by_field_name": "destination.port" + "by_field_name": "destination.port", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4005", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Port Activity" + "security_app_display_name": "Unusual Linux Network Port Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json index a20a508391fb94..dea5fa3a5db312 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json @@ -1,65 +1,30 @@ { "description": "Security: Linux - Looks for processes that are unusual to all Linux hosts. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "512mb", - "categorization_examples_limit": 4 - + "model_memory_limit": "512mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4003", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Process for a Linux Population" + "security_app_display_name": "Anomalous Process for a Linux Population", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json index 72be89bd79aadd..05d46860b145f9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json @@ -1,64 +1,30 @@ { "description": "Security: Linux - Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4008", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Username" + "security_app_display_name": "Unusual Linux Username", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json index 1481b7a03a5596..fccfa9493e8c27 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system network configuration discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network configuration discovery to increase their understanding of connected networks and hosts. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "40012", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Network Configuration Discovery" + "security_app_display_name": "Unusual Linux Network Configuration Discovery", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json index 2b1cf43ac94d3e..32dc04c079db18 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system network connection discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network connection discovery to increase their understanding of connected services and systems. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4013", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Connection Discovery" + "security_app_display_name": "Unusual Linux Network Connection Discovery", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json index fcec32acd69b50..6897876ad6ba38 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json @@ -1,46 +1,30 @@ { "description": "Security: Linux - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "process.name" - ] + "influencers": ["host.name", "user.name", "process.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4009", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "security_app_display_name": "Unusual Linux Process Calling the Metadata Service" + "security_app_display_name": "Unusual Linux Process Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json index d8414c8bf22bd3..ad81023d693836 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json @@ -1,45 +1,30 @@ { "description": "Security: Linux - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name" - ] + "influencers": ["host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4010", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "security_app_display_name": "Unusual Linux User Calling the Metadata Service" + "security_app_display_name": "Unusual Linux User Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json index a99e5f95572f7d..11be6277c42209 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for sudo activity from an unusual user context. Unusual user context changes can be due to privilege escalation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4017", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Sudo Activity" + "security_app_display_name": "Unusual Sudo Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json index 9c8ca5316ace3f..08dbbc60d02f76 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privilege elevation via locally run exploits or malware activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.title", - "host.name", - "process.working_directory", - "user.name" - ] + "influencers": ["process.title", "host.name", "process.working_directory", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,24 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4018", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Linux Compiler Activity" + "security_app_display_name": "Anomalous Linux Compiler Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json index 0202854934285a..255d0347654b04 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system information discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system information discovery to gather detailed information about system configuration and software versions. This may be a precursor to the selection of a persistence mechanism or a method of privilege elevation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4014", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Information Discovery Activity" + "security_app_display_name": "Unusual Linux System Information Discovery Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json index 23e6e607ccf080..03e57ce2237af9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system process discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system process discovery to increase their understanding of software applications running on a target host or network. This may be a precursor to the selection of a persistence mechanism or a method of privilege elevation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4015", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Process Discovery Activity" + "security_app_display_name": "Unusual Linux Process Discovery Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json index 8659e7a8f1f91d..2b1c4dc5957775 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system user or owner discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system owner or user discovery to identify currently active or primary users of a system. This may be a precursor to additional discovery, credential dumping, or privilege elevation activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4016", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Owner or User Discovery Activity" + "security_app_display_name": "Unusual Linux User Discovery Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json index a072007a0f13cb..ce0e7f413f6763 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json @@ -1,65 +1,31 @@ { "description": "Security: Linux - Looks for processes that are unusual to a particular Linux host. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each host.name, detects rare process.name values.", + "detector_description": "Detects rare processes for a host.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "host.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4002", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Process for a Linux Host" + "security_app_display_name": "Unusual Process for a Linux Host", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json index bed522d4e954a5..edf6c66a213bd7 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json @@ -2,7 +2,7 @@ "id": "security_network", "title": "Security: Network", "description": "Detect anomalous network activity in your ECS-compatible network logs.", - "type": "network data", + "type": "Network data", "logoFile": "logo.json", "defaultIndexPattern": "logs-*,filebeat-*,packetbeat-*", "query": { @@ -14,7 +14,7 @@ } } ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json index 4479fe8f8c6627..b19a3f0e27812b 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_non_zero_count by \"destination.geo.country_name\"", + "detector_description": "Detects high count by country.", "function": "high_non_zero_count", "by_field_name": "destination.geo.country_name", "detector_index": 0 @@ -19,8 +16,7 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,6 +27,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Network Traffic to a Country" + "security_app_display_name": "Spike in Network Traffic to a Country", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json index 984bfea22fa2db..1477e951d3ce9a 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_count", + "detector_description": "Detects high count of network denies.", "function": "high_count", "detector_index": 0 } @@ -18,8 +15,7 @@ "destination.as.organization.name", "source.ip", "destination.port" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +26,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Firewall Denies" + "security_app_display_name": "Spike in Firewall Denies", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json index ba740d581a27ee..81b516204fbc18 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_count", + "detector_description": "Detects high count of network events.", "function": "high_count", "detector_index": 0 } @@ -18,8 +15,7 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +26,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Network Traffic" + "security_app_display_name": "Spike in Network Traffic", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json index 123b802c475fb0..4b8799d65b7463 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json @@ -1,14 +1,11 @@ { "description": "Security: Network - looks for an unusual destination country name in the network logs. This can be due to initial access, persistence, command-and-control, or exfiltration activity. For example, when a user clicks on a link in a phishing email or opens a malicious document, a request may be sent to download and run a payload from a server in a country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"destination.geo.country_name\"", + "detector_description": "Detects rare country names.", "function": "rare", "by_field_name": "destination.geo.country_name", "detector_index": 0 @@ -30,6 +27,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Network Traffic to Rare Destination Country" + "security_app_display_name": "Network Traffic to Rare Destination Country", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json index f7a65d0137f265..799363b8fbac11 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json @@ -1,16 +1,14 @@ { "id": "security_packetbeat", "title": "Security: Packetbeat", - "description": "Detect suspicious network activity in Packetbeat data.", + "description": "Detect suspicious activity in Packetbeat data.", "type": "Packetbeat data", "logoFile": "logo.json", - "defaultIndexPattern": "packetbeat-*", + "defaultIndexPattern": "packetbeat-*,logs-*", "query": { "bool": { - "filter": [ - {"term": {"agent.type": "packetbeat"}} - ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json index 449c8af238b567..334435732a07e6 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "dns"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "dns" } }, + { "term": { "event.dataset": "network_traffic.dns" } } ], - "must_not": [ - {"bool": {"filter": {"term": {"destination.ip": "169.254.169.254"}}}} - ] + "minimum_should_match": 1, + "must_not": [{ "bool": { "filter": { "term": { "destination.ip": "169.254.169.254" } } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json index 3a4055eb55ba07..fe87d86ee352f5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "dns"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "dns" } }, + { "term": { "event.dataset": "network_traffic.dns" } } ], - "must_not": [ - {"bool": {"filter": {"term": {"dns.question.type": "PTR"}}}} - ] + "minimum_should_match": 1, + "must_not": [{ "bool": { "filter": { "term": { "dns.question.type": "PTR" } } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json index 5986c326ea80f4..79a297595d8d70 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "http"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "http" } }, + { "term": { "event.dataset": "network_traffic.http" } } ], - "must_not": [ - {"wildcard": {"user_agent.original": {"value": "Mozilla*"}}} - ] + "minimum_should_match": 1, + "must_not": [{ "wildcard": { "user_agent.original": { "value": "Mozilla*" } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json index 313bd8e1bea398..54b8ddf2e7a146 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json @@ -1,23 +1,17 @@ { "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "dns" - ], + "groups": ["security", "packetbeat", "dns"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_info_content(\"dns.question.name\") over tld", + "detector_description": "Detects high info content of DNS questions over a population of TLDs.", "function": "high_info_content", "field_name": "dns.question.name", "over_field_name": "dns.question.etld_plus_one", "custom_rules": [ { - "actions": [ - "skip_result" - ], + "actions": ["skip_result"], "conditions": [ { "applies_to": "actual", @@ -29,12 +23,7 @@ ] } ], - "influencers": [ - "destination.ip", - "host.name", - "dns.question.etld_plus_one" - ], - "model_prune_window": "30d" + "influencers": ["destination.ip", "host.name", "dns.question.etld_plus_one"] }, "allow_lazy_open": true, "analysis_limits": { @@ -45,12 +34,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "DNS Tunneling" + "security_app_display_name": "DNS Tunneling", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json index 36c8b3acd722e6..049d4e3babd23e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json @@ -1,22 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control activity.", - "groups": [ - "security", - "packetbeat", - "dns" - ], + "groups": ["security", "packetbeat", "dns"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"dns.question.name\"", + "detector_description": "Detects rare DNS question names.", "function": "rare", "by_field_name": "dns.question.name" } ], - "influencers": [ - "host.name" - ] + "influencers": ["host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -27,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual DNS Activity" + "security_app_display_name": "Unusual DNS Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json index 3f3c137e8fd345..d8df5c4986b99f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json @@ -1,24 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual HTTP or TLS destination domain activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"server.domain\"", + "detector_description": "Detects rare server domains.", "function": "rare", "by_field_name": "server.domain" } ], - "influencers": [ - "host.name", - "destination.ip", - "source.ip" - ] + "influencers": ["host.name", "destination.ip", "source.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Network Destination Domain Name" + "security_app_display_name": "Unusual Network Destination Domain Name", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json index afa430bd835f21..055204dd1c3763 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json @@ -1,23 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual web browsing URL activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"url.full\"", + "detector_description": "Detects rare URLs.", "function": "rare", "by_field_name": "url.full" } ], - "influencers": [ - "host.name", - "destination.ip" - ] + "influencers": ["host.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Web Request" + "security_app_display_name": "Unusual Web Request", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json index bb2d524b41c1f2..c947e4f1d509bb 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json @@ -1,23 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual HTTP user agent activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"user_agent.original\"", + "detector_description": "Detects rare web user agents.", "function": "rare", "by_field_name": "user_agent.original" } ], - "influencers": [ - "host.name", - "destination.ip" - ] + "influencers": ["host.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Web User Agent" + "security_app_display_name": "Unusual Web User Agent", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json index 6b7e5dcf56f1f5..38fa9e2e4e9040 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json @@ -1,67 +1,30 @@ { "description": "Security: Windows - Looks for processes that are unusual to a particular Windows host. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each host.name, detects rare process.name values.", + "detector_description": "Detects rare processes per host.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "host.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8001", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Process for a Windows Host" + "security_app_display_name": "Unusual Process for a Windows Host", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json index 04ee9912c15e3a..2e04fa91be3367 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", - "groups": [ - "endpoint", - "network", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "64mb", - "categorization_examples_limit": 4 + "model_memory_limit": "64mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8003", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Network Activity" + "security_app_display_name": "Unusual Windows Network Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json index d5c931b3c46e85..c9f0579309c6b5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json @@ -1,65 +1,29 @@ { "description": "Security: Windows - Looks for activity in unusual paths that may indicate execution of malware or persistence mechanisms. Windows payloads often execute from user profile paths.", - "groups": [ - "endpoint", - "network", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.working_directory values.", + "detector_description": "Detects rare working directories.", "function": "rare", "by_field_name": "process.working_directory", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8004", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Path Activity" + "security_app_display_name": "Unusual Windows Path Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json index 1474763cec7b95..08baa6587f9ffc 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Looks for processes that are unusual to all Windows hosts. Such unusual processes may indicate execution of unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.executable values.", + "detector_description": "Detects rare process executable values.", "function": "rare", - "by_field_name": "process.executable", + "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8002", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Process for a Windows Population" + "security_app_display_name": "Anomalous Process for a Windows Population", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json index 2966630fad878f..1bf46c2d416a9d 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json @@ -1,67 +1,30 @@ { "description": "Security: Windows - Looks for unusual process relationships which may indicate execution of malware or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each process.parent.name, detects rare process.name values.", + "detector_description": "Detects rare processes per parent process.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "process.parent.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8005", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Windows Process Creation" + "security_app_display_name": "Anomalous Windows Process Creation", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json index b01641b2ef3add..5472ad77e1b706 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json @@ -1,28 +1,17 @@ { "description": "Security: Windows - Looks for unusual powershell scripts that may indicate execution of malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "windows", - "winlogbeat", - "powershell", - "security" - ], + "groups": ["windows", "powershell", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects high information content in powershell.file.script_block_text values.", + "detector_description": "Detects high information content in powershell scripts.", "function": "high_info_content", - "field_name": "powershell.file.script_block_text" + "field_name": "powershell.file.script_block_text", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "file.path" - ] + "influencers": ["host.name", "user.name", "file.path"] }, "allow_lazy_open": true, "analysis_limits": { @@ -32,24 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8006", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Suspicious Powershell Script" + "security_app_display_name": "Suspicious Powershell Script", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json index 9716c8365e317c..b2530538a92631 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json @@ -1,27 +1,17 @@ { - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "description": "Security: Windows - Looks for rare and unusual Windows service names which may indicate execution of unauthorized services, malware, or persistence mechanisms.", "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare winlog.event_data.ServiceName values.", + "detector_description": "Detects rare service names.", "function": "rare", - "by_field_name": "winlog.event_data.ServiceName" + "by_field_name": "winlog.event_data.ServiceName", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "winlog.event_data.ServiceName" - ] + "influencers": ["host.name", "winlog.event_data.ServiceName"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,20 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8007", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Service" + "security_app_display_name": "Unusual Windows Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json index eda4b768b53081..659e58cfdba32e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8008", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Username" + "security_app_display_name": "Unusual Windows Username", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json index ab4fd311d66460..953a00a8fff522 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json @@ -1,47 +1,29 @@ { "description": "Security: Windows - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "security", - "endpoint", - "process", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare process names.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "user.name" - ] + "influencers": ["process.name", "host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8011", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "security_app_display_name": "Unusual Windows Process Calling the Metadata Service" + "security_app_display_name": "Unusual Windows Process Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json index fe8a634d499214..df55cb3d677097 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json @@ -1,46 +1,29 @@ { "description": "Security: Windows - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "endpoint", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name" - ] + "influencers": ["host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8012", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "security_app_display_name": "Unusual Windows User Calling the Metadata Service" + "security_app_display_name": "Unusual Windows User Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json index b95aa1144f4402..87d9d4b172f63e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json @@ -1,27 +1,16 @@ { "description": "Security: Windows - Unusual user context switches can be due to privilege escalation.", - "groups": [ - "endpoint", - "event-log", - "security", - "windows", - "winlogbeat", - "authentication" - ], + "groups": ["security", "windows", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name" } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +20,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8009", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows User Privilege Elevation Activity" + "security_app_display_name": "Unusual Windows User Privilege Elevation Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json index a6ec19401190fd..e118f761453be3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json @@ -1,27 +1,16 @@ { "description": "Security: Windows - Unusual RDP (remote desktop protocol) user logins can indicate account takeover or credentialed access.", - "groups": [ - "endpoint", - "event-log", - "security", - "windows", - "winlogbeat", - "authentication" - ], + "groups": ["security", "windows", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name" } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +20,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8013", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Remote User" + "security_app_display_name": "Unusual Windows Remote User", + "managed": true, + "job_revision": 4 } } From 069324a823b2f7e2306c68ab81f913e9fd80472b Mon Sep 17 00:00:00 2001 From: Navarone Feekery <13634519+navarone-feekery@users.noreply.github.com> Date: Mon, 24 Apr 2023 15:59:36 +0200 Subject: [PATCH 46/65] [Enterprise Search] Use caching for filtered config fields (#155608) Moves the configurable fields filtering to the logic file so it can make use of caching. --- .../connector_configuration_form.tsx | 28 +- .../connector_configuration_logic.test.ts | 348 +++++++++++++++++- .../connector_configuration_logic.ts | 29 +- 3 files changed, 376 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx index 7627c5c869469d..2c40eb0beafa4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx @@ -24,16 +24,12 @@ import { import { i18n } from '@kbn/i18n'; import { Status } from '../../../../../../common/types/api'; -import { DependencyLookup, DisplayType } from '../../../../../../common/types/connectors'; +import { DisplayType } from '../../../../../../common/types/connectors'; import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic'; import { ConnectorConfigurationField } from './connector_configuration_field'; -import { - ConfigEntry, - ConnectorConfigurationLogic, - dependenciesSatisfied, -} from './connector_configuration_logic'; +import { ConnectorConfigurationLogic } from './connector_configuration_logic'; export const ConnectorConfigurationForm = () => { const { status } = useValues(ConnectorConfigurationApiLogic); @@ -41,20 +37,6 @@ export const ConnectorConfigurationForm = () => { const { localConfigView } = useValues(ConnectorConfigurationLogic); const { saveConfig, setIsEditing } = useActions(ConnectorConfigurationLogic); - const dependencyLookup: DependencyLookup = localConfigView.reduce( - (prev: Record, configEntry: ConfigEntry) => ({ - ...prev, - [configEntry.key]: configEntry.value, - }), - {} - ); - - const filteredConfigView = localConfigView.filter( - (configEntry) => - configEntry.ui_restrictions.length <= 0 && - dependenciesSatisfied(configEntry.depends_on, dependencyLookup) - ); - return ( { @@ -63,7 +45,7 @@ export const ConnectorConfigurationForm = () => { }} component="form" > - {filteredConfigView.map((configEntry, index) => { + {localConfigView.map((configEntry, index) => { const { default_value: defaultValue, depends_on: dependencies, @@ -94,8 +76,8 @@ export const ConnectorConfigurationForm = () => { if (dependencies.length > 0) { // dynamic spacing without CSS - const previousField = filteredConfigView[index - 1]; - const nextField = filteredConfigView[index + 1]; + const previousField = localConfigView[index - 1]; + const nextField = localConfigView[index + 1]; const topSpacing = !previousField || previousField.depends_on.length <= 0 ? : <>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts index 64e7d1af9c9994..f87d73b882ecdd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts @@ -154,7 +154,7 @@ describe('ConnectorConfigurationLogic', () => { }); }); describe('setLocalConfigEntry', () => { - it('should set local config entry and sort keys', () => { + it('should set local config entry, and sort and filter keys', () => { ConnectorConfigurationLogic.actions.setConfigState({ bar: { default_value: '', @@ -182,6 +182,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }); ConnectorConfigurationLogic.actions.setLocalConfigState({ bar: { @@ -210,6 +281,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }); ConnectorConfigurationLogic.actions.setLocalConfigEntry({ default_value: '', @@ -254,6 +396,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }, configView: [ { @@ -284,6 +497,37 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + key: 'shownDependent1', + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + key: 'shownDependent2', + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, ], localConfigState: { bar: { @@ -312,6 +556,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }, localConfigView: [ { @@ -342,6 +657,37 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + key: 'hiddenDependent1', + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + key: 'hiddenDependent2', + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, ], }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts index 84b0fd4d23fdb8..861ab90079229f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts @@ -72,12 +72,17 @@ export interface ConfigEntry { /** * - * Sorts the connector configuration by specified order (if present) + * Sorts and filters the connector configuration + * + * Sorting is done by specified order (if present) * otherwise by alphabetic order of keys * + * Filtering is done on any fields with ui_restrictions + * or that have not had their dependencies met + * */ -function sortConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry[] { - return Object.keys(config) +function sortAndFilterConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry[] { + const sortedConfig = Object.keys(config) .map( (key) => ({ @@ -98,6 +103,20 @@ function sortConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry } return a.key.localeCompare(b.key); }); + + const dependencyLookup: DependencyLookup = sortedConfig.reduce( + (prev: Record, configEntry: ConfigEntry) => ({ + ...prev, + [configEntry.key]: configEntry.value, + }), + {} + ); + + return sortedConfig.filter( + (configEntry) => + configEntry.ui_restrictions.length <= 0 && + dependenciesSatisfied(configEntry.depends_on, dependencyLookup) + ); } export function ensureStringType(value: string | number | boolean | null): string { @@ -280,11 +299,11 @@ export const ConnectorConfigurationLogic = kea< selectors: ({ selectors }) => ({ configView: [ () => [selectors.configState], - (configState: ConnectorConfiguration) => sortConnectorConfiguration(configState), + (configState: ConnectorConfiguration) => sortAndFilterConnectorConfiguration(configState), ], localConfigView: [ () => [selectors.localConfigState], - (configState) => sortConnectorConfiguration(configState), + (configState) => sortAndFilterConnectorConfiguration(configState), ], }), }); From 54457b074a20da8017de03feb9ebfbe0fe6450d3 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 24 Apr 2023 11:13:57 -0300 Subject: [PATCH 47/65] [Infrastructure UI] Plot metric charts data based on current page items (#155249) closes [#152186](https://github.com/elastic/kibana/issues/152186) ## Summary This PR makes the metric charts show data for the hosts on the current page. With this change, the charts will **only** load after the table has finished loading its data - or after Snapshot API has responded It also changes the current behavior of the table pagination and sorting. Instead of relying on the `EuiInMemoryTable` the pagination and sorting are done manually, and the EuiInMemoryTable has been replaced by the `EuiBasicTable`. The loading indicator has also been replaced. Paginating and sorting: https://user-images.githubusercontent.com/2767137/233161166-2bd719e1-7259-4ecc-96a7-50493bc6c0a3.mov Open in lens https://user-images.githubusercontent.com/2767137/233161134-621afd76-44b5-42ab-b58c-7f51ef944ac2.mov ### How to test - Go to Hosts view - Paginate and sort the table data - Select a page size and check if the select has been stored in the localStorage (`hostsView:pageSizeSelection` key) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../hosts/components/chart/chart_loader.tsx | 58 ++++++ .../hosts/components/chart/lens_wrapper.tsx | 87 ++++---- .../components/chart/metric_chart_wrapper.tsx | 53 ++--- .../metadata/metadata.test.tsx | 42 +--- .../hosts/components/hosts_container.tsx | 33 +-- .../metrics/hosts/components/hosts_table.tsx | 127 +++++------- .../hosts/components/kpis/kpi_grid.tsx | 5 +- .../metrics/hosts/components/kpis/tile.tsx | 15 +- .../components/tabs/logs/logs_tab_content.tsx | 5 +- .../components/tabs/metrics/metric_chart.tsx | 63 ++++-- .../public/pages/metrics/hosts/constants.ts | 3 + .../hosts/hooks/use_after_loaded_state.ts | 26 +++ .../metrics/hosts/hooks/use_alerts_query.ts | 2 +- .../hosts/hooks/use_hosts_table.test.ts | 192 +++++++++--------- .../metrics/hosts/hooks/use_hosts_table.tsx | 112 ++++++++-- .../hosts/hooks/use_hosts_table_url_state.ts | 94 +++++++++ .../hooks/use_table_properties_url_state.ts | 62 ------ .../infra/public/pages/metrics/hosts/utils.ts | 17 +- .../test/functional/apps/infra/hosts_view.ts | 83 ++++++++ .../page_objects/infra_hosts_view.ts | 47 +++++ 20 files changed, 716 insertions(+), 410 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts delete mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx new file mode 100644 index 00000000000000..bbddb338ef73f8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiProgress, EuiFlexItem, EuiLoadingChart, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; + +export const ChartLoader = ({ + children, + loading, + style, + loadedOnce = false, + hasTitle = false, +}: { + style?: React.CSSProperties; + children: React.ReactNode; + loadedOnce: boolean; + loading: boolean; + hasTitle?: boolean; +}) => { + const { euiTheme } = useEuiTheme(); + return ( + + {loading && ( + + )} + {loading && !loadedOnce ? ( + + + + + + ) : ( + children + )} + + ); +}; + +const LoaderContainer = euiStyled.div` + position: relative; + border-radius: ${({ theme }) => theme.eui.euiSizeS}; + overflow: hidden; + height: 100%; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx index 9985db0751fd45..9a2472949f54cf 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx @@ -4,18 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiLoadingChart } from '@elastic/eui'; import { Filter, Query, TimeRange } from '@kbn/es-query'; import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; import { useIntersectedOnce } from '../../../../../hooks/use_intersection_once'; import { LensAttributes } from '../../../../../common/visualizations'; +import { ChartLoader } from './chart_loader'; export interface Props { id: string; @@ -26,7 +24,10 @@ export interface Props { extraActions: Action[]; lastReloadRequestTime?: number; style?: React.CSSProperties; + loading?: boolean; + hasTitle?: boolean; onBrushEnd?: (data: BrushTriggerEvent['data']) => void; + onLoad?: () => void; } export const LensWrapper = ({ @@ -39,12 +40,19 @@ export const LensWrapper = ({ style, onBrushEnd, lastReloadRequestTime, + loading = false, + hasTitle = false, }: Props) => { - const intersectionRef = React.useRef(null); + const intersectionRef = useRef(null); + const [loadedOnce, setLoadedOnce] = useState(false); + + const [state, setState] = useState({ + lastReloadRequestTime, + query, + filters, + dateRange, + }); - const [currentLastReloadRequestTime, setCurrentLastReloadRequestTime] = useState< - number | undefined - >(lastReloadRequestTime); const { services: { lens }, } = useKibanaContextForPlugin(); @@ -56,38 +64,49 @@ export const LensWrapper = ({ useEffect(() => { if ((intersection?.intersectionRatio ?? 0) === 1) { - setCurrentLastReloadRequestTime(lastReloadRequestTime); + setState({ + lastReloadRequestTime, + query, + dateRange, + filters, + }); } - }, [intersection?.intersectionRatio, lastReloadRequestTime]); + }, [dateRange, filters, intersection?.intersectionRatio, lastReloadRequestTime, query]); const isReady = attributes && intersectedOnce; return (
- {!isReady ? ( - - - - - - ) : ( - - )} + + {isReady && ( + { + if (!loadedOnce) { + setLoadedOnce(true); + } + }} + /> + )} +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx index 9df937983ae1eb..8d78906bd03e97 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx @@ -14,16 +14,11 @@ import { } from '@elastic/charts'; import { EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; -import { EuiLoadingChart } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; -import { EuiProgress } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { useEuiTheme } from '@elastic/eui'; import type { SnapshotNode, SnapshotNodeMetric } from '../../../../../../common/http_api'; import { createInventoryMetricFormatter } from '../../../inventory_view/lib/create_inventory_metric_formatter'; import type { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; +import { ChartLoader } from './chart_loader'; type MetricType = keyof Pick; @@ -65,7 +60,6 @@ export const MetricChartWrapper = ({ type, ...props }: Props) => { - const { euiTheme } = useEuiTheme(); const loadedOnce = useRef(false); const metrics = useMemo(() => (nodes ?? [])[0]?.metrics ?? [], [nodes]); const metricsTimeseries = useMemo( @@ -109,39 +103,18 @@ export const MetricChartWrapper = ({ return ( -
- {loading && ( - - )} - {loading && !loadedOnce.current ? ( - - - - - - ) : ( - - - - - - )} -
+ + + + + + +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx index 1c6320c142d7ab..46392fa8609d19 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx @@ -32,42 +32,12 @@ const metadataProps: TabProps = { name: 'host-1', cloudProvider: 'gcp', }, - rx: { - name: 'rx', - value: 0, - max: 0, - avg: 0, - }, - tx: { - name: 'tx', - value: 0, - max: 0, - avg: 0, - }, - memory: { - name: 'memory', - value: 0.5445920331099282, - max: 0.5445920331099282, - avg: 0.5445920331099282, - }, - cpu: { - name: 'cpu', - value: 0.2000718443867342, - max: 0.2000718443867342, - avg: 0.2000718443867342, - }, - diskLatency: { - name: 'diskLatency', - value: null, - max: 0, - avg: 0, - }, - memoryTotal: { - name: 'memoryTotal', - value: 16777216, - max: 16777216, - avg: 16777216, - }, + rx: 0, + tx: 0, + memory: 0.5445920331099282, + cpu: 0.2000718443867342, + diskLatency: 0, + memoryTotal: 16777216, }, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx index e8e8a8a8e7c4f7..0c965feca8e9ec 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx @@ -12,10 +12,11 @@ import { InfraLoadingPanel } from '../../../../components/loading'; import { useMetricsDataViewContext } from '../hooks/use_data_view'; import { UnifiedSearchBar } from './unified_search_bar'; import { HostsTable } from './hosts_table'; -import { HostsViewProvider } from '../hooks/use_hosts_view'; +import { KPIGrid } from './kpis/kpi_grid'; import { Tabs } from './tabs/tabs'; import { AlertsQueryProvider } from '../hooks/use_alerts_query'; -import { KPIGrid } from './kpis/kpi_grid'; +import { HostsViewProvider } from '../hooks/use_hosts_view'; +import { HostsTableProvider } from '../hooks/use_hosts_table'; export const HostContainer = () => { const { dataView, loading, hasError } = useMetricsDataViewContext(); @@ -38,19 +39,21 @@ export const HostContainer = () => { - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index ca6f904ceea847..535afe8befff50 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -5,93 +5,78 @@ * 2.0. */ -import React, { useCallback } from 'react'; -import { EuiInMemoryTable } from '@elastic/eui'; +import React from 'react'; +import { EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEqual } from 'lodash'; import { NoData } from '../../../../components/empty_states'; -import { InfraLoadingPanel } from '../../../../components/loading'; -import { useHostsTable } from '../hooks/use_hosts_table'; -import { useTableProperties } from '../hooks/use_table_properties_url_state'; +import { HostNodeRow, useHostsTableContext } from '../hooks/use_hosts_table'; import { useHostsViewContext } from '../hooks/use_hosts_view'; import { useUnifiedSearchContext } from '../hooks/use_unified_search'; import { Flyout } from './host_details_flyout/flyout'; +import { DEFAULT_PAGE_SIZE } from '../constants'; -export const HostsTable = () => { - const { hostNodes, loading } = useHostsViewContext(); - const { onSubmit, searchCriteria } = useUnifiedSearchContext(); - const [properties, setProperties] = useTableProperties(); - - const { columns, items, isFlyoutOpen, closeFlyout, clickedItem } = useHostsTable(hostNodes, { - time: searchCriteria.dateRange, - }); - - const noData = items.length === 0; - - const onTableChange = useCallback( - ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field, direction } = sort; - - const sorting = field && direction ? { field, direction } : true; - const pagination = pageIndex >= 0 && pageSize !== 0 ? { pageIndex, pageSize } : true; - - if (!isEqual(properties.sorting, sorting)) { - setProperties({ sorting }); - } - if (!isEqual(properties.pagination, pagination)) { - setProperties({ pagination }); - } - }, - [setProperties, properties.pagination, properties.sorting] - ); +const PAGE_SIZE_OPTIONS = [5, 10, 20]; - if (loading) { - return ( - - ); - } +export const HostsTable = () => { + const { loading } = useHostsViewContext(); + const { onSubmit } = useUnifiedSearchContext(); - if (noData) { - return ( - onSubmit()} - testString="noMetricsDataPrompt" - /> - ); - } + const { + columns, + items, + currentPage, + isFlyoutOpen, + closeFlyout, + clickedItem, + onTableChange, + pagination, + sorting, + } = useHostsTableContext(); return ( <> - onSubmit()} + testString="noMetricsDataPrompt" + /> + ) + } /> {isFlyoutOpen && clickedItem && } diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx index 968e7462b38f41..2dbd0c4324ecac 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx @@ -6,11 +6,8 @@ */ import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; - +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { KPIChartProps, Tile } from './tile'; import { HostsTile } from './hosts_tile'; import { ChartBaseProps } from '../chart/metric_chart_wrapper'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx index 480e6c415dc459..a95f18b4a10ee8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx @@ -8,13 +8,16 @@ import React from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiI18n } from '@elastic/eui'; +import { + EuiIcon, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiI18n, + EuiToolTip, +} from '@elastic/eui'; import styled from 'styled-components'; -import { EuiToolTip } from '@elastic/eui'; import { useLensAttributes } from '../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index 0fad370960f223..d5cc0b0f021d76 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -24,7 +24,10 @@ export const LogsTabContent = () => { const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]); const { hostNodes, loading } = useHostsViewContext(); - const hostsFilterQuery = useMemo(() => createHostsFilter(hostNodes), [hostNodes]); + const hostsFilterQuery = useMemo( + () => createHostsFilter(hostNodes.map((p) => p.name)), + [hostNodes] + ); const logsLinkToStreamQuery = useMemo(() => { const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx index 252bea5389e3ab..28d07b94d94377 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx @@ -4,20 +4,28 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiI18n } from '@elastic/eui'; +import { + EuiIcon, + EuiPanel, + EuiI18n, + EuiFlexGroup, + EuiFlexItem, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; import { useLensAttributes } from '../../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; import { HostsLensLineChartFormulas } from '../../../../../../common/visualizations'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; +import { createHostsFilter } from '../../../utils'; +import { useHostsTableContext } from '../../../hooks/use_hosts_table'; import { LensWrapper } from '../../chart/lens_wrapper'; +import { useAfterLoadedState } from '../../../hooks/use_after_loaded_state'; export interface MetricChartProps { title: string; @@ -29,9 +37,18 @@ export interface MetricChartProps { const MIN_HEIGHT = 300; export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => { + const { euiTheme } = useEuiTheme(); const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest } = useHostsViewContext(); + const { baseRequest, loading } = useHostsViewContext(); + const { currentPage } = useHostsTableContext(); + + // prevents updates on requestTs and serchCriteria states from relaoding the chart + // we want it to reload only once the table has finished loading + const { afterLoadedState } = useAfterLoadedState(loading, { + lastReloadRequestTime: baseRequest.requestTs, + ...searchCriteria, + }); const { attributes, getExtraActions, error } = useLensAttributes({ type, @@ -43,11 +60,22 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => visualizationType: 'lineChart', }); - const filters = [...searchCriteria.filters, ...searchCriteria.panelFilters]; + const hostsFilterQuery = useMemo(() => { + return createHostsFilter( + currentPage.map((p) => p.name), + dataView + ); + }, [currentPage, dataView]); + + const filters = [ + ...afterLoadedState.filters, + ...afterLoadedState.panelFilters, + ...[hostsFilterQuery], + ]; const extraActionOptions = getExtraActions({ - timeRange: searchCriteria.dateRange, + timeRange: afterLoadedState.dateRange, filters, - query: searchCriteria.query, + query: afterLoadedState.query, }); const extraActions: Action[] = [extraActionOptions.openInLens]; @@ -69,12 +97,15 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => hasShadow={false} hasBorder paddingSize={error ? 'm' : 'none'} - style={{ minHeight: MIN_HEIGHT }} + css={css` + min-height: calc(${MIN_HEIGHT} + ${euiTheme.size.l}); + position: 'relative'; + `} data-test-subj={`hostsView-metricChart-${type}`} > {error ? ( attributes={attributes} style={{ height: MIN_HEIGHT }} extraActions={extraActions} - lastReloadRequestTime={baseRequest.requestTs} - dateRange={searchCriteria.dateRange} + lastReloadRequestTime={afterLoadedState.lastReloadRequestTime} + dateRange={afterLoadedState.dateRange} filters={filters} - query={searchCriteria.query} + query={afterLoadedState.query} onBrushEnd={handleBrushEnd} + loading={loading} + hasTitle /> )} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index 98aa8a145e3a0a..b854120a868879 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -13,6 +13,9 @@ export const ALERT_STATUS_ALL = 'all'; export const TIMESTAMP_FIELD = '@timestamp'; export const DATA_VIEW_PREFIX = 'infra_metrics'; +export const DEFAULT_PAGE_SIZE = 10; +export const LOCAL_STORAGE_PAGE_SIZE_KEY = 'hostsView:pageSizeSelection'; + export const ALL_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_ALL, label: i18n.translate('xpack.infra.hostsViewPage.tabs.alerts.alertStatusFilter.showAll', { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts new file mode 100644 index 00000000000000..8c9a84d4402f81 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef } from 'react'; + +export const useAfterLoadedState = (loading: boolean, state: T) => { + const ref = useRef(undefined); + const [internalState, setInternalState] = useState(state); + + if (!ref.current || loading !== ref.current) { + ref.current = loading; + } + + useEffect(() => { + if (!loading) { + setInternalState(state); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref.current]); + + return { afterLoadedState: internalState }; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts index 9877d616437218..7a895591d68c7f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts @@ -69,7 +69,7 @@ const createAlertsEsQuery = ({ const alertStatusFilter = createAlertStatusFilter(status); const dateFilter = createDateFilter(dateRange); - const hostsFilter = createHostsFilter(hostNodes); + const hostsFilter = createHostsFilter(hostNodes.map((p) => p.name)); const filters = [alertStatusFilter, dateFilter, hostsFilter].filter(Boolean) as Filter[]; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts index 4ae8823adaf2ed..a921a0daeb011f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts @@ -8,68 +8,92 @@ import { useHostsTable } from './use_hosts_table'; import { renderHook } from '@testing-library/react-hooks'; import { SnapshotNode } from '../../../../../common/http_api'; +import * as useUnifiedSearchHooks from './use_unified_search'; +import * as useHostsViewHooks from './use_hosts_view'; -describe('useHostTable hook', () => { - it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { - const nodes: SnapshotNode[] = [ +jest.mock('./use_unified_search'); +jest.mock('./use_hosts_view'); + +const mockUseUnifiedSearchContext = + useUnifiedSearchHooks.useUnifiedSearchContext as jest.MockedFunction< + typeof useUnifiedSearchHooks.useUnifiedSearchContext + >; +const mockUseHostsViewContext = useHostsViewHooks.useHostsViewContext as jest.MockedFunction< + typeof useHostsViewHooks.useHostsViewContext +>; + +const mockHostNode: SnapshotNode[] = [ + { + metrics: [ { - metrics: [ - { - name: 'rx', - avg: 252456.92916666667, - }, - { - name: 'tx', - avg: 252758.425, - }, - { - name: 'memory', - avg: 0.94525, - }, - { - name: 'cpu', - value: 0.6353277777777777, - }, - { - name: 'memoryTotal', - avg: 34359.738368, - }, - ], - path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], - name: 'host-0', + name: 'rx', + avg: 252456.92916666667, }, { - metrics: [ - { - name: 'rx', - avg: 95.86339715321859, - }, - { - name: 'tx', - avg: 110.38566859563191, - }, - { - name: 'memory', - avg: 0.5400000214576721, - }, - { - name: 'cpu', - value: 0.8647805555555556, - }, - { - name: 'memoryTotal', - avg: 9.194304, - }, - ], - path: [ - { value: 'host-1', label: 'host-1' }, - { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, - ], - name: 'host-1', + name: 'tx', + avg: 252758.425, }, - ]; + { + name: 'memory', + avg: 0.94525, + }, + { + name: 'cpu', + value: 0.6353277777777777, + }, + { + name: 'memoryTotal', + avg: 34359.738368, + }, + ], + path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], + name: 'host-0', + }, + { + metrics: [ + { + name: 'rx', + avg: 95.86339715321859, + }, + { + name: 'tx', + avg: 110.38566859563191, + }, + { + name: 'memory', + avg: 0.5400000214576721, + }, + { + name: 'cpu', + value: 0.8647805555555556, + }, + { + name: 'memoryTotal', + avg: 9.194304, + }, + ], + path: [ + { value: 'host-1', label: 'host-1' }, + { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, + ], + name: 'host-1', + }, +]; + +describe('useHostTable hook', () => { + beforeAll(() => { + mockUseUnifiedSearchContext.mockReturnValue({ + searchCriteria: { + dateRange: { from: 'now-15m', to: 'now' }, + }, + } as ReturnType); - const items = [ + mockUseHostsViewContext.mockReturnValue({ + hostNodes: mockHostNode, + } as ReturnType); + }); + it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { + const expected = [ { name: 'host-0', os: '-', @@ -79,27 +103,11 @@ describe('useHostTable hook', () => { cloudProvider: 'aws', name: 'host-0', }, - rx: { - name: 'rx', - avg: 252456.92916666667, - }, - tx: { - name: 'tx', - avg: 252758.425, - }, - memory: { - name: 'memory', - avg: 0.94525, - }, - cpu: { - name: 'cpu', - value: 0.6353277777777777, - }, - memoryTotal: { - name: 'memoryTotal', - - avg: 34359.738368, - }, + rx: 252456.92916666667, + tx: 252758.425, + memory: 0.94525, + cpu: 0.6353277777777777, + memoryTotal: 34359.738368, }, { name: 'host-1', @@ -110,32 +118,16 @@ describe('useHostTable hook', () => { cloudProvider: null, name: 'host-1', }, - rx: { - name: 'rx', - avg: 95.86339715321859, - }, - tx: { - name: 'tx', - avg: 110.38566859563191, - }, - memory: { - name: 'memory', - avg: 0.5400000214576721, - }, - cpu: { - name: 'cpu', - value: 0.8647805555555556, - }, - memoryTotal: { - name: 'memoryTotal', - avg: 9.194304, - }, + rx: 95.86339715321859, + tx: 110.38566859563191, + memory: 0.5400000214576721, + cpu: 0.8647805555555556, + memoryTotal: 9.194304, }, ]; - const time = { from: 'now-15m', to: 'now', interval: '>=1m' }; - const { result } = renderHook(() => useHostsTable(nodes, { time })); + const { result } = renderHook(() => useHostsTable()); - expect(result.current.items).toStrictEqual(items); + expect(result.current.items).toStrictEqual(expected); }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 44a492f314c1ce..2d2d6c9d7f8e44 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -8,8 +8,10 @@ import React, { useCallback, useMemo } from 'react'; import { EuiBasicTableColumn, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { TimeRange } from '@kbn/es-query'; - +import createContainer from 'constate'; +import { isEqual } from 'lodash'; +import { CriteriaWithPagination } from '@elastic/eui'; +import { isNumber } from 'lodash/fp'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter'; import { HostsTableEntryTitle } from '../components/hosts_table_entry_title'; @@ -19,6 +21,9 @@ import type { SnapshotMetricInput, } from '../../../../../common/http_api'; import { useHostFlyoutOpen } from './use_host_flyout_open_url_state'; +import { Sorting, useHostsTableProperties } from './use_hosts_table_url_state'; +import { useHostsViewContext } from './use_hosts_view'; +import { useUnifiedSearchContext } from './use_unified_search'; /** * Columns and items types @@ -27,7 +32,7 @@ export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider'; type HostMetric = 'cpu' | 'diskLatency' | 'rx' | 'tx' | 'memory' | 'memoryTotal'; -type HostMetrics = Record; +type HostMetrics = Record; export interface HostNodeRow extends HostMetrics { os?: string | null; @@ -38,10 +43,6 @@ export interface HostNodeRow extends HostMetrics { id: string; } -interface HostTableParams { - time: TimeRange; -} - /** * Helper functions */ @@ -60,12 +61,41 @@ const buildItemsList = (nodes: SnapshotNode[]) => { cloudProvider: path.at(-1)?.cloudProvider ?? null, }, ...metrics.reduce((data, metric) => { - data[metric.name as HostMetric] = metric; + data[metric.name as HostMetric] = metric.avg ?? metric.value; return data; }, {} as HostMetrics), })) as HostNodeRow[]; }; +const isTitleColumn = (cell: any): cell is HostNodeRow['title'] => { + return typeof cell === 'object' && cell && 'name' in cell; +}; + +const sortValues = (aValue: any, bValue: any, { direction }: Sorting) => { + if (typeof aValue === 'string' && typeof bValue === 'string') { + return direction === 'desc' ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue); + } + + if (isNumber(aValue) && isNumber(bValue)) { + return direction === 'desc' ? bValue - aValue : aValue - bValue; + } + + return 1; +}; + +const sortTableData = + ({ direction, field }: Sorting) => + (a: HostNodeRow, b: HostNodeRow) => { + const aValue = a[field as keyof HostNodeRow]; + const bValue = b[field as keyof HostNodeRow]; + + if (isTitleColumn(aValue) && isTitleColumn(bValue)) { + return sortValues(aValue.name, bValue.name, { direction, field }); + } + + return sortValues(aValue, bValue, { direction, field }); + }; + /** * Columns translations */ @@ -120,7 +150,10 @@ const toggleDialogActionLabel = i18n.translate( /** * Build a table columns and items starting from the snapshot nodes. */ -export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) => { +export const useHostsTable = () => { + const { hostNodes } = useHostsViewContext(); + const { searchCriteria } = useUnifiedSearchContext(); + const [{ pagination, sorting }, setProperties] = useHostsTableProperties(); const { services: { telemetry }, } = useKibanaContextForPlugin(); @@ -139,12 +172,38 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) [telemetry] ); - const items = useMemo(() => buildItemsList(nodes), [nodes]); + const onTableChange = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + const { index: pageIndex, size: pageSize } = page; + const { field, direction } = sort ?? {}; + + const currentSorting = { field: field as keyof HostNodeRow, direction }; + const currentPagination = { pageIndex, pageSize }; + + if (!isEqual(sorting, currentSorting)) { + setProperties({ sorting: currentSorting }); + } else if (!isEqual(pagination, currentPagination)) { + setProperties({ pagination: currentPagination }); + } + }, + [setProperties, pagination, sorting] + ); + + const items = useMemo(() => buildItemsList(hostNodes), [hostNodes]); const clickedItem = useMemo( () => items.find(({ id }) => id === hostFlyoutOpen.clickedItemId), [hostFlyoutOpen.clickedItemId, items] ); + const currentPage = useMemo(() => { + const { pageSize = 0, pageIndex = 0 } = pagination; + + const endIndex = (pageIndex + 1) * pageSize; + const startIndex = pageIndex * pageSize; + + return items.sort(sortTableData(sorting)).slice(startIndex, endIndex); + }, [items, pagination, sorting]); + const columns: Array> = useMemo( () => [ { @@ -183,7 +242,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) render: (title: HostNodeRow['title']) => ( reportHostEntryClick(title)} /> ), @@ -197,7 +256,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageCpuUsageLabel, - field: 'cpu.avg', + field: 'cpu', sortable: true, 'data-test-subj': 'hostsView-tableRow-cpuUsage', render: (avg: number) => formatMetric('cpu', avg), @@ -205,7 +264,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: diskLatencyLabel, - field: 'diskLatency.avg', + field: 'diskLatency', sortable: true, 'data-test-subj': 'hostsView-tableRow-diskLatency', render: (avg: number) => formatMetric('diskLatency', avg), @@ -213,7 +272,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageRXLabel, - field: 'rx.avg', + field: 'rx', sortable: true, 'data-test-subj': 'hostsView-tableRow-rx', render: (avg: number) => formatMetric('rx', avg), @@ -221,7 +280,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageTXLabel, - field: 'tx.avg', + field: 'tx', sortable: true, 'data-test-subj': 'hostsView-tableRow-tx', render: (avg: number) => formatMetric('tx', avg), @@ -229,7 +288,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageTotalMemoryLabel, - field: 'memoryTotal.avg', + field: 'memoryTotal', sortable: true, 'data-test-subj': 'hostsView-tableRow-memoryTotal', render: (avg: number) => formatMetric('memoryTotal', avg), @@ -237,21 +296,34 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageMemoryUsageLabel, - field: 'memory.avg', + field: 'memory', sortable: true, 'data-test-subj': 'hostsView-tableRow-memory', render: (avg: number) => formatMetric('memory', avg), align: 'right', }, ], - [hostFlyoutOpen.clickedItemId, reportHostEntryClick, setFlyoutClosed, setHostFlyoutOpen, time] + [ + hostFlyoutOpen.clickedItemId, + reportHostEntryClick, + searchCriteria.dateRange, + setFlyoutClosed, + setHostFlyoutOpen, + ] ); return { columns, - items, clickedItem, - isFlyoutOpen: !!hostFlyoutOpen.clickedItemId, + currentPage, closeFlyout, + items, + isFlyoutOpen: !!hostFlyoutOpen.clickedItemId, + onTableChange, + pagination, + sorting, }; }; + +export const HostsTable = createContainer(useHostsTable); +export const [HostsTableProvider, useHostsTableContext] = HostsTable; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts new file mode 100644 index 00000000000000..b4889d62f58783 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import deepEqual from 'fast-deep-equal'; +import { useReducer } from 'react'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { DEFAULT_PAGE_SIZE, LOCAL_STORAGE_PAGE_SIZE_KEY } from '../constants'; + +export const GET_DEFAULT_TABLE_PROPERTIES: TableProperties = { + sorting: { + direction: 'asc', + field: 'name', + }, + pagination: { + pageIndex: 0, + pageSize: DEFAULT_PAGE_SIZE, + }, +}; + +const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; + +const reducer = (prevState: TableProperties, params: Payload) => { + const payload = Object.fromEntries(Object.entries(params).filter(([_, v]) => !!v)); + + return { + ...prevState, + ...payload, + }; +}; + +export const useHostsTableProperties = (): [TableProperties, TablePropertiesUpdater] => { + const [localStoragePageSize, setLocalStoragePageSize] = useLocalStorage( + LOCAL_STORAGE_PAGE_SIZE_KEY, + DEFAULT_PAGE_SIZE + ); + + const [urlState, setUrlState] = useUrlState({ + defaultState: { + ...GET_DEFAULT_TABLE_PROPERTIES, + pagination: { + ...GET_DEFAULT_TABLE_PROPERTIES.pagination, + pageSize: localStoragePageSize, + }, + }, + + decodeUrlState, + encodeUrlState, + urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, + }); + + const [properties, setProperties] = useReducer(reducer, urlState); + if (!deepEqual(properties, urlState)) { + setUrlState(properties); + if (localStoragePageSize !== properties.pagination.pageSize) { + setLocalStoragePageSize(properties.pagination.pageSize); + } + } + + return [properties, setProperties]; +}; + +const PaginationRT = rt.partial({ pageIndex: rt.number, pageSize: rt.number }); +const SortingRT = rt.intersection([ + rt.type({ + field: rt.string, + }), + rt.partial({ direction: rt.union([rt.literal('asc'), rt.literal('desc')]) }), +]); + +const TableStateRT = rt.type({ + pagination: PaginationRT, + sorting: SortingRT, +}); + +export type TableState = rt.TypeOf; +export type Payload = Partial; +export type TablePropertiesUpdater = (params: Payload) => void; + +export type Sorting = rt.TypeOf; +type TableProperties = rt.TypeOf; + +const encodeUrlState = TableStateRT.encode; +const decodeUrlState = (value: unknown) => { + return pipe(TableStateRT.decode(value), fold(constant(undefined), identity)); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts deleted file mode 100644 index 980fdf19a684c5..00000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts +++ /dev/null @@ -1,62 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { constant, identity } from 'fp-ts/lib/function'; -import { useUrlState } from '../../../../utils/use_url_state'; - -export const GET_DEFAULT_TABLE_PROPERTIES = { - sorting: true, - pagination: true, -}; -const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; - -type Action = rt.TypeOf; -type PropertiesUpdater = (newProps: Action) => void; - -export const useTableProperties = (): [TableProperties, PropertiesUpdater] => { - const [urlState, setUrlState] = useUrlState({ - defaultState: GET_DEFAULT_TABLE_PROPERTIES, - decodeUrlState, - encodeUrlState, - urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, - }); - - const setProperties = (newProps: Action) => setUrlState({ ...urlState, ...newProps }); - - return [urlState, setProperties]; -}; - -const PaginationRT = rt.union([ - rt.boolean, - rt.partial({ pageIndex: rt.number, pageSize: rt.number }), -]); -const SortingRT = rt.union([rt.boolean, rt.type({ field: rt.string, direction: rt.any })]); - -const SetSortingRT = rt.partial({ - sorting: SortingRT, -}); - -const SetPaginationRT = rt.partial({ - pagination: PaginationRT, -}); - -const ActionRT = rt.intersection([SetSortingRT, SetPaginationRT]); - -const TablePropertiesRT = rt.type({ - pagination: PaginationRT, - sorting: SortingRT, -}); - -type TableProperties = rt.TypeOf; - -const encodeUrlState = TablePropertiesRT.encode; -const decodeUrlState = (value: unknown) => { - return pipe(TablePropertiesRT.decode(value), fold(constant(undefined), identity)); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts index a04fdfa46b279a..5da9d36b0f5876 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts @@ -5,16 +5,23 @@ * 2.0. */ -import { Filter } from '@kbn/es-query'; -import { SnapshotNode } from '../../../../common/http_api'; +import { DataViewBase, Filter } from '@kbn/es-query'; -export const createHostsFilter = (hostNodes: SnapshotNode[]): Filter => { +export const createHostsFilter = (hostNames: string[], dataView?: DataViewBase): Filter => { return { query: { terms: { - 'host.name': hostNodes.map((p) => p.name), + 'host.name': hostNames, }, }, - meta: {}, + meta: dataView + ? { + value: hostNames.join(), + type: 'phrases', + params: hostNames, + index: dataView.id, + key: 'host.name', + } + : {}, }; }; diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 3cf0091c93bd4f..e9000a9cf3e6db 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -529,6 +529,89 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Pagination and Sorting', () => { + beforeEach(async () => { + await pageObjects.infraHostsView.changePageSize(5); + }); + + it('should show 5 rows on the first page', async () => { + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row, position) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[position])); + }); + }); + + it('should paginate to the last page', async () => { + await pageObjects.infraHostsView.paginateTo(2); + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[5])); + }); + }); + + it('should show all hosts on the same page', async () => { + await pageObjects.infraHostsView.changePageSize(10); + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row, position) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[position])); + }); + }); + + it('should sort by Disk Latency asc', async () => { + await pageObjects.infraHostsView.sortByDiskLatency(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[0]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[1]); + }); + + it('should sort by Disk Latency desc', async () => { + await pageObjects.infraHostsView.sortByDiskLatency(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[1]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[0]); + }); + + it('should sort by Title asc', async () => { + await pageObjects.infraHostsView.sortByTitle(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[0]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[5]); + }); + + it('should sort by Title desc', async () => { + await pageObjects.infraHostsView.sortByTitle(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[5]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[0]); + }); + }); }); }); }; diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index ae0cc601f8cc7d..6478d208226ad8 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -241,6 +241,7 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { async typeInQueryBar(query: string) { const queryBar = await this.getQueryBar(); + await queryBar.clearValueWithKeyboard(); return queryBar.type(query); }, @@ -249,5 +250,51 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { await testSubjects.click('querySubmitButton'); }, + + // Pagination + getPageNumberButton(pageNumber: number) { + return testSubjects.find(`pagination-button-${pageNumber - 1}`); + }, + + getPageSizeSelector() { + return testSubjects.find('tablePaginationPopoverButton'); + }, + + getPageSizeOption(pageSize: number) { + return testSubjects.find(`tablePagination-${pageSize}-rows`); + }, + + async changePageSize(pageSize: number) { + const pageSizeSelector = await this.getPageSizeSelector(); + await pageSizeSelector.click(); + const pageSizeOption = await this.getPageSizeOption(pageSize); + await pageSizeOption.click(); + }, + + async paginateTo(pageNumber: number) { + const paginationButton = await this.getPageNumberButton(pageNumber); + await paginationButton.click(); + }, + + // Sorting + getDiskLatencyHeader() { + return testSubjects.find('tableHeaderCell_diskLatency_4'); + }, + + getTitleHeader() { + return testSubjects.find('tableHeaderCell_title_1'); + }, + + async sortByDiskLatency() { + const diskLatency = await this.getDiskLatencyHeader(); + const button = await testSubjects.findDescendant('tableHeaderSortButton', diskLatency); + return button.click(); + }, + + async sortByTitle() { + const titleHeader = await this.getTitleHeader(); + const button = await testSubjects.findDescendant('tableHeaderSortButton', titleHeader); + return button.click(); + }, }; } From 111d04f45a64cc050407bd9f892e1f77ddd8cc9f Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Mon, 24 Apr 2023 16:27:38 +0200 Subject: [PATCH 48/65] [APM] Add transaction name filter in failed transaction rate rule type (#155405) part of https://github.com/elastic/kibana/issues/152329 related work https://github.com/elastic/kibana/pull/154241 Introduces the Transaction name filter in the failed transaction rate rule type https://user-images.githubusercontent.com/3369346/233386404-1875b283-0321-4bf1-a7d3-66327f7d4ec5.mov ## Fixes The regression introduces in a previous [PR](https://github.com/elastic/kibana/pull/154241/commits/fce4ef8168429645a01434e19b0feaefba1a4f02) Existing rule types can have empty string in their params so we need to make sure we don't filter empty values as it will yield no results. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/common/rules/schema.ts | 1 + .../index.stories.tsx | 105 ++++++++++++++++++ .../index.tsx | 16 ++- .../register_error_count_rule_type.ts | 8 +- ...register_transaction_duration_rule_type.ts | 12 +- ...et_transaction_error_rate_chart_preview.ts | 13 ++- ...gister_transaction_error_rate_rule_type.ts | 16 ++- .../tests/alerts/chart_preview.spec.ts | 55 +++++++++ 8 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx diff --git a/x-pack/plugins/apm/common/rules/schema.ts b/x-pack/plugins/apm/common/rules/schema.ts index 698b4507c5b3f3..ca77e76f6f1565 100644 --- a/x-pack/plugins/apm/common/rules/schema.ts +++ b/x-pack/plugins/apm/common/rules/schema.ts @@ -52,6 +52,7 @@ export const transactionErrorRateParamsSchema = schema.object({ windowUnit: schema.string(), threshold: schema.number(), transactionType: schema.maybe(schema.string()), + transactionName: schema.maybe(schema.string()), serviceName: schema.maybe(schema.string()), environment: schema.string(), }); diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx new file mode 100644 index 00000000000000..cd94439db0389c --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Story } from '@storybook/react'; +import React, { ComponentType, useState } from 'react'; +import { CoreStart } from '@kbn/core/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { RuleParams, TransactionErrorRateRuleType } from '.'; +import { AlertMetadata } from '../../utils/helper'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; + +const KibanaReactContext = createKibanaReactContext({ + notifications: { toasts: { add: () => {} } }, +} as unknown as Partial); + +interface Args { + ruleParams: RuleParams; + metadata?: AlertMetadata; +} + +export default { + title: 'alerting/TransactionErrorRateRuleType', + component: TransactionErrorRateRuleType, + decorators: [ + (StoryComponent: ComponentType) => { + return ( + +
+ +
+
+ ); + }, + ], +}; + +export const CreatingInApmServiceOverview: Story = ({ + ruleParams, + metadata, +}) => { + const [params, setParams] = useState(ruleParams); + + function setRuleParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; + +CreatingInApmServiceOverview.args = { + ruleParams: { + environment: 'testEnvironment', + serviceName: 'testServiceName', + threshold: 1500, + transactionType: 'testTransactionType', + transactionName: 'GET /api/customer/:id', + windowSize: 5, + windowUnit: 'm', + }, + metadata: { + environment: ENVIRONMENT_ALL.value, + serviceName: undefined, + }, +}; + +export const CreatingInStackManagement: Story = ({ + ruleParams, + metadata, +}) => { + const [params, setParams] = useState(ruleParams); + + function setRuleParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; + +CreatingInStackManagement.args = { + ruleParams: { + environment: 'testEnvironment', + threshold: 1500, + windowSize: 5, + windowUnit: 'm', + }, + metadata: undefined, +}; diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx index f9cfd6a511ef20..f161ef085b3eab 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx @@ -23,20 +23,22 @@ import { IsAboveField, ServiceField, TransactionTypeField, + TransactionNameField, } from '../../utils/fields'; import { AlertMetadata, getIntervalAndTimeRange } from '../../utils/helper'; import { ApmRuleParamsContainer } from '../../ui_components/apm_rule_params_container'; -interface RuleParams { +export interface RuleParams { windowSize?: number; windowUnit?: string; threshold?: number; serviceName?: string; transactionType?: string; + transactionName?: string; environment?: string; } -interface Props { +export interface Props { ruleParams: RuleParams; metadata?: AlertMetadata; setRuleParams: (key: string, value: any) => void; @@ -78,6 +80,7 @@ export function TransactionErrorRateRuleType(props: Props) { environment: params.environment, serviceName: params.serviceName, transactionType: params.transactionType, + transactionName: params.transactionName, interval, start, end, @@ -89,6 +92,7 @@ export function TransactionErrorRateRuleType(props: Props) { }, [ params.transactionType, + params.transactionName, params.environment, params.serviceName, params.windowSize, @@ -102,7 +106,8 @@ export function TransactionErrorRateRuleType(props: Props) { onChange={(value) => { if (value !== params.serviceName) { setRuleParams('serviceName', value); - setRuleParams('transactionType', ''); + setRuleParams('transactionType', undefined); + setRuleParams('transactionName', undefined); setRuleParams('environment', ENVIRONMENT_ALL.value); } }} @@ -117,6 +122,11 @@ export function TransactionErrorRateRuleType(props: Props) { onChange={(value) => setRuleParams('environment', value)} serviceName={params.serviceName} />, + setRuleParams('transactionName', value)} + serviceName={params.serviceName} + />, { - const { serviceName, environment, transactionType, interval, start, end } = - alertParams; + const { + serviceName, + environment, + transactionType, + interval, + start, + end, + transactionName, + } = alertParams; const searchAggregatedTransactions = await getSearchTransactionsEvents({ config, @@ -62,6 +70,7 @@ export async function getTransactionErrorRateChartPreview({ filter: [ ...termQuery(SERVICE_NAME, serviceName), ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions( diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 7ceaf8ca780486..26b5847a205f12 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -32,6 +32,7 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_TYPE, + TRANSACTION_NAME, } from '../../../../../common/es_fields/apm'; import { EventOutcome } from '../../../../../common/event_outcome'; import { @@ -86,6 +87,7 @@ export function registerTransactionErrorRateRuleType({ apmActionVariables.interval, apmActionVariables.reason, apmActionVariables.serviceName, + apmActionVariables.transactionName, apmActionVariables.threshold, apmActionVariables.transactionType, apmActionVariables.triggerValue, @@ -142,8 +144,15 @@ export function registerTransactionErrorRateRuleType({ ], }, }, - ...termQuery(SERVICE_NAME, ruleParams.serviceName), - ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType), + ...termQuery(SERVICE_NAME, ruleParams.serviceName, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_NAME, ruleParams.transactionName, { + queryEmptyString: false, + }), ...environmentQuery(ruleParams.environment), ], }, @@ -232,6 +241,7 @@ export function registerTransactionErrorRateRuleType({ serviceName, transactionType, environment, + ruleParams.transactionName, ] .filter((name) => name) .join('_'); @@ -255,6 +265,7 @@ export function registerTransactionErrorRateRuleType({ [SERVICE_NAME]: serviceName, ...getEnvironmentEsField(environment), [TRANSACTION_TYPE]: transactionType, + [TRANSACTION_NAME]: ruleParams.transactionName, [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: errorRate, [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, @@ -272,6 +283,7 @@ export function registerTransactionErrorRateRuleType({ serviceName, threshold: ruleParams.threshold, transactionType, + transactionName: ruleParams.transactionName, triggerValue: asDecimalOrInteger(errorRate), viewInAppUrl, }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts index 7ec09849b7ff22..f95bb8de59a894 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts @@ -83,6 +83,61 @@ export default function ApiTest({ getService }: FtrProviderContext) { ).to.equal(true); }); + it('transaction_error_rate with transaction name', async () => { + const options = { + params: { + query: { + start, + end, + serviceName: 'opbeans-java', + transactionName: 'APIRestController#product', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview[0]).to.eql({ + x: 1627974600000, + y: 1, + }); + }); + + it('transaction_error_rate with nonexistent transaction name', async () => { + const options = { + params: { + query: { + start, + end, + serviceName: 'opbeans-java', + transactionName: 'foo', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.every( + (item: { x: number; y: number | null }) => item.y === null + ) + ).to.equal(true); + }); + it('error_count (with data)', async () => { const options = getOptions(); options.params.query.transactionType = undefined; From 1095375fe39e960d0569d818191bef468c93a44b Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 24 Apr 2023 16:42:37 +0200 Subject: [PATCH 49/65] [Cases] Close FilePreview with Escape key. (#155592) Fixes #155036 ## Summary Allow users to close the file preview in cases by using the Escape key. (e2e coming in a different PR with other tests) --- .../components/files/file_preview.test.tsx | 20 +++++++++++++++++++ .../public/components/files/file_preview.tsx | 18 +++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx index b02df3a82228f4..c1d7fe20bee483 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import type { AppMockRenderer } from '../../common/mock'; @@ -35,4 +36,23 @@ describe('FilePreview', () => { expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); }); + + it('pressing escape calls closePreview', async () => { + const closePreview = jest.fn(); + + appMockRender.render(); + + await waitFor(() => + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + id: basicFileMock.id, + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + }) + ); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + + userEvent.keyboard('{esc}'); + + await waitFor(() => expect(closePreview).toHaveBeenCalled()); + }); }); diff --git a/x-pack/plugins/cases/public/components/files/file_preview.tsx b/x-pack/plugins/cases/public/components/files/file_preview.tsx index 1bb91c5b53ff74..09cee1320ec2a7 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.tsx @@ -4,12 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import type { FileJSON } from '@kbn/shared-ux-file-types'; -import { EuiOverlayMask, EuiFocusTrap, EuiImage } from '@elastic/eui'; +import { EuiOverlayMask, EuiFocusTrap, EuiImage, keys } from '@elastic/eui'; import { useFilesContext } from '@kbn/shared-ux-file-context'; import type { Owner } from '../../../common/constants/types'; @@ -36,6 +36,20 @@ export const FilePreview = ({ closePreview, selectedFile }: FilePreviewProps) => const { client: filesClient } = useFilesContext(); const { owner } = useCasesContext(); + useEffect(() => { + const keyboardListener = (event: KeyboardEvent) => { + if (event.key === keys.ESCAPE || event.code === 'Escape') { + closePreview(); + } + }; + + window.addEventListener('keyup', keyboardListener); + + return () => { + window.removeEventListener('keyup', keyboardListener); + }; + }, [closePreview]); + return ( From a03d20be039d1c449b2848f46463bc423b6f5183 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 24 Apr 2023 15:51:36 +0100 Subject: [PATCH 50/65] skip flaky suite (#154970) --- .../sections/alerts_table/bulk_actions/bulk_actions.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx index f9d209549da0c2..23fac59fca2081 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx @@ -691,7 +691,8 @@ describe('AlertsTable.BulkActions', () => { ).toBeTruthy(); }); - describe('and clear the selection is clicked', () => { + // FLAKY: https://github.com/elastic/kibana/issues/154970 + describe.skip('and clear the selection is clicked', () => { it('should turn off the toolbar', async () => { const props = { ...tablePropsWithBulkActions, From 2c14b584f8f736f65211b5f738f9e0d764681346 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 24 Apr 2023 15:55:40 +0100 Subject: [PATCH 51/65] skip flaky suite (#155222) --- x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index 92320dad62087a..54b1baae454bde 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -209,7 +209,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); } - describe('explain log rate spikes', async function () { + // FLAKY: https://github.com/elastic/kibana/issues/155222 + describe.skip('explain log rate spikes', async function () { for (const testData of explainLogRateSpikesTestData) { describe(`with '${testData.sourceIndexOrSavedSearch}'`, function () { before(async () => { From 3d78370aa584e179ae9e9d30fabe080242812d22 Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Mon, 24 Apr 2023 11:02:43 -0400 Subject: [PATCH 52/65] Fix API links when generating API key snippet (#155435) Fixes the Search Applications API page to set an URL to the ES plugin rather than Enterprise Search URL. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../engine_connect/engine_api_integration.tsx | 19 ++++++++++--------- .../engine_connect/search_application_api.tsx | 14 +++++++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx index b61614838d7a1c..2fe691e262b64b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx @@ -12,23 +12,24 @@ import { useValues } from 'kea'; import { EuiCodeBlock, EuiSpacer, EuiText, EuiTabs, EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; - +import { useCloudDetails } from '../../../../shared/cloud_details/cloud_details'; import { EngineViewLogic } from '../engine_view_logic'; import { EngineApiLogic } from './engine_api_logic'; -const SearchUISnippet = (enterpriseSearchUrl: string, engineName: string, apiKey: string) => ` +import { elasticsearchUrl } from './search_application_api'; + +const SearchUISnippet = (esUrl: string, engineName: string, apiKey: string) => `6 import EnginesAPIConnector from "@elastic/search-ui-engines-connector"; const connector = new EnginesAPIConnector({ - host: "${enterpriseSearchUrl}", + host: "${esUrl}", engineName: "${engineName}", apiKey: "${apiKey || ''}" });`; -const cURLSnippet = (enterpriseSearchUrl: string, engineName: string, apiKey: string) => ` -curl --location --request GET '${enterpriseSearchUrl}/api/engines/${engineName}/_search' \\ +const cURLSnippet = (esUrl: string, engineName: string, apiKey: string) => ` +curl --location --request GET '${esUrl}/${engineName}/_search' \\ --header 'Authorization: apiKey ${apiKey || ''}' \\ --header 'Content-Type: application/json' \\ --data-raw '{ @@ -47,19 +48,19 @@ interface Tab { export const EngineApiIntegrationStage: React.FC = () => { const [selectedTab, setSelectedTab] = React.useState('curl'); const { engineName } = useValues(EngineViewLogic); - const enterpriseSearchUrl = getEnterpriseSearchUrl(); const { apiKey } = useValues(EngineApiLogic); + const cloudContext = useCloudDetails(); const Tabs: Record = { curl: { - code: cURLSnippet(enterpriseSearchUrl, engineName, apiKey), + code: cURLSnippet(elasticsearchUrl(cloudContext), engineName, apiKey), language: 'bash', title: i18n.translate('xpack.enterpriseSearch.content.engine.api.step3.curlTitle', { defaultMessage: 'cURL', }), }, searchui: { - code: SearchUISnippet(enterpriseSearchUrl, engineName, apiKey), + code: SearchUISnippet(elasticsearchUrl(cloudContext), engineName, apiKey), language: 'javascript', title: i18n.translate('xpack.enterpriseSearch.content.engine.api.step3.searchUITitle', { defaultMessage: 'Search UI', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx index 9d3c27895657fc..6934de4051bdb4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx @@ -23,9 +23,10 @@ import { i18n } from '@kbn/i18n'; import { ANALYTICS_PLUGIN } from '../../../../../../common/constants'; import { COLLECTION_INTEGRATE_PATH } from '../../../../analytics/routes'; +import { CloudDetails, useCloudDetails } from '../../../../shared/cloud_details/cloud_details'; +import { decodeCloudId } from '../../../../shared/decode_cloud_id/decode_cloud_id'; import { docLinks } from '../../../../shared/doc_links'; import { generateEncodedPath } from '../../../../shared/encode_path_params'; -import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; import { KibanaLogic } from '../../../../shared/kibana'; import { EngineViewLogic } from '../engine_view_logic'; @@ -34,12 +35,19 @@ import { EngineApiIntegrationStage } from './engine_api_integration'; import { EngineApiLogic } from './engine_api_logic'; import { GenerateEngineApiKeyModal } from './generate_engine_api_key_modal/generate_engine_api_key_modal'; +export const elasticsearchUrl = (cloudContext: CloudDetails): string => { + const defaultUrl = 'https://localhost:9200'; + const url = + (cloudContext.cloudId && decodeCloudId(cloudContext.cloudId)?.elasticsearchUrl) || defaultUrl; + return url; +}; + export const SearchApplicationAPI = () => { const { engineName } = useValues(EngineViewLogic); const { isGenerateModalOpen } = useValues(EngineApiLogic); const { openGenerateModal, closeGenerateModal } = useActions(EngineApiLogic); - const enterpriseSearchUrl = getEnterpriseSearchUrl(); const { navigateToUrl } = useValues(KibanaLogic); + const cloudContext = useCloudDetails(); const steps = [ { @@ -132,7 +140,7 @@ export const SearchApplicationAPI = () => { - {enterpriseSearchUrl} + {elasticsearchUrl(cloudContext)} From 29a10fddc9af9246f6a329a9aa2018b0a907505d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 24 Apr 2023 16:45:41 +0100 Subject: [PATCH 53/65] [Files] Allow option to disable delete action in mgt UI (#155179) --- .../content-management/table_list/index.ts | 2 +- .../table_list/src/components/table.tsx | 33 +++++- .../table_list/src/index.ts | 2 + .../table_list/src/table_list_view.test.tsx | 105 ++++++++++++++++++ .../table_list/src/table_list_view.tsx | 35 +++++- .../table_list/src/types.ts | 13 +++ .../shared-ux/file/types/base_file_client.ts | 1 + packages/shared-ux/file/types/index.ts | 16 +++ src/plugins/files/public/plugin.ts | 11 +- .../adapters/query_filters.ts | 16 +++ .../server/file_service/file_action_types.ts | 4 + .../integration_tests/file_service.test.ts | 21 +++- src/plugins/files/server/routes/find.ts | 4 +- src/plugins/files_management/public/app.tsx | 28 ++++- .../files_management/public/context.tsx | 11 +- .../files_management/public/i18n_texts.ts | 3 + .../public/mount_management_section.tsx | 8 +- src/plugins/files_management/public/types.ts | 2 + 18 files changed, 294 insertions(+), 21 deletions(-) diff --git a/packages/content-management/table_list/index.ts b/packages/content-management/table_list/index.ts index 532b35450d5418..9a608b2d6dda3a 100644 --- a/packages/content-management/table_list/index.ts +++ b/packages/content-management/table_list/index.ts @@ -8,5 +8,5 @@ export { TableListView, TableListViewProvider, TableListViewKibanaProvider } from './src'; -export type { UserContentCommonSchema } from './src'; +export type { UserContentCommonSchema, RowActions } from './src'; export type { TableListViewKibanaDependencies } from './src/services'; diff --git a/packages/content-management/table_list/src/components/table.tsx b/packages/content-management/table_list/src/components/table.tsx index 330eb67be42780..3214e7bf00a72d 100644 --- a/packages/content-management/table_list/src/components/table.tsx +++ b/packages/content-management/table_list/src/components/table.tsx @@ -17,7 +17,9 @@ import { SearchFilterConfig, Direction, Query, + type EuiTableSelectionType, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useServices } from '../services'; import type { Action } from '../actions'; @@ -26,6 +28,7 @@ import type { Props as TableListViewProps, UserContentCommonSchema, } from '../table_list_view'; +import type { TableItemsRowActions } from '../types'; import { TableSortSelect } from './table_sort_select'; import { TagFilterPanel } from './tag_filter_panel'; import { useTagFilterPanel } from './use_tag_filter_panel'; @@ -51,6 +54,7 @@ interface Props extends State, TagManageme tableColumns: Array>; hasUpdatedAtMetadata: boolean; deleteItems: TableListViewProps['deleteItems']; + tableItemsRowActions: TableItemsRowActions; onSortChange: (column: SortColumnField, direction: Direction) => void; onTableChange: (criteria: CriteriaWithPagination) => void; onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void; @@ -70,6 +74,7 @@ export function Table({ entityName, entityNamePlural, tagsToTableItemMap, + tableItemsRowActions, deleteItems, tableCaption, onTableChange, @@ -105,13 +110,32 @@ export function Table({ ); }, [deleteItems, dispatch, entityName, entityNamePlural, selectedIds.length]); - const selection = deleteItems - ? { + const selection = useMemo | undefined>(() => { + if (deleteItems) { + return { onSelectionChange: (obj: T[]) => { dispatch({ type: 'onSelectionChange', data: obj }); }, - } - : undefined; + selectable: (obj) => { + const actions = tableItemsRowActions[obj.id]; + return actions?.delete?.enabled !== false; + }, + selectableMessage: (selectable, obj) => { + if (!selectable) { + const actions = tableItemsRowActions[obj.id]; + return ( + actions?.delete?.reason ?? + i18n.translate('contentManagement.tableList.actionsDisabledLabel', { + defaultMessage: 'Actions disabled for this item', + }) + ); + } + return ''; + }, + initialSelected: [], + }; + } + }, [deleteItems, dispatch, tableItemsRowActions]); const { isPopoverOpen, @@ -214,6 +238,7 @@ export function Table({ data-test-subj="itemsInMemTable" rowHeader="attributes.title" tableCaption={tableCaption} + isSelectable /> ); } diff --git a/packages/content-management/table_list/src/index.ts b/packages/content-management/table_list/src/index.ts index df0d1e22bc1067..d1e83d7dd2e93f 100644 --- a/packages/content-management/table_list/src/index.ts +++ b/packages/content-management/table_list/src/index.ts @@ -15,3 +15,5 @@ export type { } from './table_list_view'; export { TableListViewProvider, TableListViewKibanaProvider } from './services'; + +export type { RowActions } from './types'; diff --git a/packages/content-management/table_list/src/table_list_view.test.tsx b/packages/content-management/table_list/src/table_list_view.test.tsx index 62c83fb5b94548..0245af450fb8a3 100644 --- a/packages/content-management/table_list/src/table_list_view.test.tsx +++ b/packages/content-management/table_list/src/table_list_view.test.tsx @@ -1067,4 +1067,109 @@ describe('TableListView', () => { expect(router?.history.location?.search).toBe('?sort=title&sortdir=desc'); }); }); + + describe('row item actions', () => { + const hits: UserContentCommonSchema[] = [ + { + id: '123', + updatedAt: twoDaysAgo.toISOString(), + type: 'dashboard', + attributes: { + title: 'Item 1', + description: 'Item 1 description', + }, + references: [], + }, + { + id: '456', + updatedAt: yesterday.toISOString(), + type: 'dashboard', + attributes: { + title: 'Item 2', + description: 'Item 2 description', + }, + references: [], + }, + ]; + + const setupTest = async (props?: Partial) => { + let testBed: TestBed | undefined; + const deleteItems = jest.fn(); + await act(async () => { + testBed = await setup({ + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits }), + deleteItems, + ...props, + }); + }); + + testBed!.component.update(); + return { testBed: testBed!, deleteItems }; + }; + + test('should allow select items to be deleted', async () => { + const { + testBed: { table, find, exists, component, form }, + deleteItems, + } = await setupTest(); + + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + + expect(tableCellsValues).toEqual([ + ['', 'Item 2Item 2 description', yesterdayToString], // First empty col is the "checkbox" + ['', 'Item 1Item 1 description', twoDaysAgoToString], + ]); + + const selectedHit = hits[1]; + + expect(exists('deleteSelectedItems')).toBe(false); + act(() => { + // Select the second item + form.selectCheckBox(`checkboxSelectRow-${selectedHit.id}`); + }); + component.update(); + // Delete button is now visible + expect(exists('deleteSelectedItems')).toBe(true); + + // Click delete and validate that confirm modal opens + expect(component.exists('.euiModal--confirmation')).toBe(false); + act(() => { + find('deleteSelectedItems').simulate('click'); + }); + component.update(); + expect(component.exists('.euiModal--confirmation')).toBe(true); + + await act(async () => { + find('confirmModalConfirmButton').simulate('click'); + }); + expect(deleteItems).toHaveBeenCalledWith([selectedHit]); + }); + + test('should allow to disable the "delete" action for a row', async () => { + const reasonMessage = 'This file cannot be deleted.'; + + const { + testBed: { find }, + } = await setupTest({ + rowItemActions: (obj) => { + if (obj.id === hits[1].id) { + return { + delete: { + enabled: false, + reason: reasonMessage, + }, + }; + } + }, + }); + + const firstCheckBox = find(`checkboxSelectRow-${hits[0].id}`); + const secondCheckBox = find(`checkboxSelectRow-${hits[1].id}`); + + expect(firstCheckBox.props().disabled).toBe(false); + expect(secondCheckBox.props().disabled).toBe(true); + // EUI changes the check "title" from "Select this row" to the reason to disable the checkbox + expect(secondCheckBox.props().title).toBe(reasonMessage); + }); + }); }); diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 1612649f80bdac..2191a3c9b7eeee 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -42,6 +42,7 @@ import { getReducer } from './reducer'; import type { SortColumnField } from './components'; import { useTags } from './use_tags'; import { useInRouterContext, useUrlState } from './use_url_state'; +import { RowActions, TableItemsRowActions } from './types'; interface ContentEditorConfig extends Pick { @@ -67,6 +68,11 @@ export interface Props RowActions | undefined; children?: ReactNode | undefined; findItems( searchQuery: string, @@ -241,6 +247,7 @@ function TableListViewComp({ urlStateEnabled = true, customTableColumn, emptyPrompt, + rowItemActions, findItems, createItem, editItem, @@ -580,6 +587,15 @@ function TableListViewComp({ return selectedIds.map((selectedId) => itemsById[selectedId]); }, [selectedIds, itemsById]); + const tableItemsRowActions = useMemo(() => { + return items.reduce((acc, item) => { + return { + ...acc, + [item.id]: rowItemActions ? rowItemActions(item) : undefined, + }; + }, {}); + }, [items, rowItemActions]); + // ------------ // Callbacks // ------------ @@ -854,6 +870,20 @@ function TableListViewComp({ }; }, []); + const PageTemplate = useMemo(() => { + return withoutPageTemplateWrapper + ? ((({ + children: _children, + 'data-test-subj': dataTestSubj, + }: { + children: React.ReactNode; + ['data-test-subj']?: string; + }) => ( +
{_children}
+ )) as unknown as typeof KibanaPageTemplate) + : KibanaPageTemplate; + }, [withoutPageTemplateWrapper]); + // ------------ // Render // ------------ @@ -861,10 +891,6 @@ function TableListViewComp({ return null; } - const PageTemplate = withoutPageTemplateWrapper - ? (React.Fragment as unknown as typeof KibanaPageTemplate) - : KibanaPageTemplate; - if (!showFetchError && hasNoItems) { return ( @@ -929,6 +955,7 @@ function TableListViewComp({ tagsToTableItemMap={tagsToTableItemMap} deleteItems={deleteItems} tableCaption={tableListTitle} + tableItemsRowActions={tableItemsRowActions} onTableChange={onTableChange} onTableSearchChange={onTableSearchChange} onSortChange={onSortChange} diff --git a/packages/content-management/table_list/src/types.ts b/packages/content-management/table_list/src/types.ts index 0e716e6d59cf34..c8e734a289451f 100644 --- a/packages/content-management/table_list/src/types.ts +++ b/packages/content-management/table_list/src/types.ts @@ -12,3 +12,16 @@ export interface Tag { description: string; color: string; } + +export type TableRowAction = 'delete'; + +export type RowActions = { + [action in TableRowAction]?: { + enabled: boolean; + reason?: string; + }; +}; + +export interface TableItemsRowActions { + [id: string]: RowActions | undefined; +} diff --git a/packages/shared-ux/file/types/base_file_client.ts b/packages/shared-ux/file/types/base_file_client.ts index 4a00f2de005163..52d1ca09fd1707 100644 --- a/packages/shared-ux/file/types/base_file_client.ts +++ b/packages/shared-ux/file/types/base_file_client.ts @@ -27,6 +27,7 @@ export interface BaseFilesClient { find: ( args: { kind?: string | string[]; + kindToExclude?: string | string[]; status?: string | string[]; extension?: string | string[]; name?: string | string[]; diff --git a/packages/shared-ux/file/types/index.ts b/packages/shared-ux/file/types/index.ts index 4c49124f7149ff..86b9e47fdab43e 100644 --- a/packages/shared-ux/file/types/index.ts +++ b/packages/shared-ux/file/types/index.ts @@ -250,6 +250,22 @@ export interface FileKindBrowser extends FileKindBase { * @default 4MiB */ maxSizeBytes?: number; + /** + * Allowed actions that can be done in the File Management UI. If not provided, all actions are allowed + * + */ + managementUiActions?: { + /** Allow files to be listed in management UI */ + list?: { + enabled: boolean; + }; + /** Allow files to be deleted in management UI */ + delete?: { + enabled: boolean; + /** If delete is not enabled in management UI, specify the reason (will appear in a tooltip). */ + reason?: string; + }; + }; } /** diff --git a/src/plugins/files/public/plugin.ts b/src/plugins/files/public/plugin.ts index 54646e9199f9a8..13828d0ee366cb 100644 --- a/src/plugins/files/public/plugin.ts +++ b/src/plugins/files/public/plugin.ts @@ -35,7 +35,10 @@ export interface FilesSetup { registerFileKind(fileKind: FileKindBrowser): void; } -export type FilesStart = Pick; +export type FilesStart = Pick & { + getFileKindDefinition: (id: string) => FileKindBrowser; + getAllFindKindDefinitions: () => FileKindBrowser[]; +}; /** * Bringing files to Kibana @@ -77,6 +80,12 @@ export class FilesPlugin implements Plugin { start(core: CoreStart): FilesStart { return { filesClientFactory: this.filesClientFactory!, + getFileKindDefinition: (id: string): FileKindBrowser => { + return this.registry.get(id); + }, + getAllFindKindDefinitions: (): FileKindBrowser[] => { + return this.registry.getAll(); + }, }; } } diff --git a/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts b/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts index 0f453a1b81e6af..014e57b41d2b10 100644 --- a/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts +++ b/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts @@ -24,6 +24,7 @@ export function filterArgsToKuery({ extension, mimeType, kind, + kindToExclude, meta, name, status, @@ -50,12 +51,27 @@ export function filterArgsToKuery({ } }; + const addExcludeFilters = (fieldName: keyof FileMetadata | string, values: string[] = []) => { + if (values.length) { + const andExpressions = values + .filter(Boolean) + .map((value) => + nodeTypes.function.buildNode( + 'not', + nodeBuilder.is(`${attrPrefix}.${fieldName}`, escapeKuery(value)) + ) + ); + kueryExpressions.push(nodeBuilder.and(andExpressions)); + } + }; + addFilters('name', name, true); addFilters('FileKind', kind); addFilters('Status', status); addFilters('extension', extension); addFilters('mime_type', mimeType); addFilters('user.id', user); + addExcludeFilters('FileKind', kindToExclude); if (meta) { const addMetaFilters = pipe( diff --git a/src/plugins/files/server/file_service/file_action_types.ts b/src/plugins/files/server/file_service/file_action_types.ts index 4247f567802edc..96795ac93b3876 100644 --- a/src/plugins/files/server/file_service/file_action_types.ts +++ b/src/plugins/files/server/file_service/file_action_types.ts @@ -82,6 +82,10 @@ export interface FindFileArgs extends Pagination { * File kind(s), see {@link FileKind}. */ kind?: string[]; + /** + * File kind(s) to exclude from search, see {@link FileKind}. + */ + kindToExclude?: string[]; /** * File name(s). */ diff --git a/src/plugins/files/server/integration_tests/file_service.test.ts b/src/plugins/files/server/integration_tests/file_service.test.ts index 25d7f463de03ad..3492eb8e5f12c4 100644 --- a/src/plugins/files/server/integration_tests/file_service.test.ts +++ b/src/plugins/files/server/integration_tests/file_service.test.ts @@ -157,26 +157,39 @@ describe('FileService', () => { createDisposableFile({ fileKind, name: 'foo-2' }), createDisposableFile({ fileKind, name: 'foo-3' }), createDisposableFile({ fileKind, name: 'test-3' }), + createDisposableFile({ fileKind: fileKindNonDefault, name: 'foo-1' }), ]); { const { files, total } = await fileService.find({ - kind: [fileKind], + kind: [fileKind, fileKindNonDefault], name: ['foo*'], perPage: 2, page: 1, }); expect(files.length).toBe(2); - expect(total).toBe(3); + expect(total).toBe(4); } { const { files, total } = await fileService.find({ - kind: [fileKind], + kind: [fileKind, fileKindNonDefault], name: ['foo*'], perPage: 2, page: 2, }); - expect(files.length).toBe(1); + expect(files.length).toBe(2); + expect(total).toBe(4); + } + + // Filter out fileKind + { + const { files, total } = await fileService.find({ + kindToExclude: [fileKindNonDefault], + name: ['foo*'], + perPage: 10, + page: 1, + }); + expect(files.length).toBe(3); // foo-1 from fileKindNonDefault not returned expect(total).toBe(3); } }); diff --git a/src/plugins/files/server/routes/find.ts b/src/plugins/files/server/routes/find.ts index a81a9d2ea5220d..6749e06254100b 100644 --- a/src/plugins/files/server/routes/find.ts +++ b/src/plugins/files/server/routes/find.ts @@ -30,6 +30,7 @@ export function toArrayOrUndefined(val?: string | string[]): undefined | string[ const rt = { body: schema.object({ kind: schema.maybe(stringOrArrayOfStrings), + kindToExclude: schema.maybe(stringOrArrayOfStrings), status: schema.maybe(stringOrArrayOfStrings), extension: schema.maybe(stringOrArrayOfStrings), name: schema.maybe(nameStringOrArrayOfNameStrings), @@ -50,12 +51,13 @@ export type Endpoint = CreateRouteDefinition< const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; const { - body: { meta, extension, kind, name, status }, + body: { meta, extension, kind, name, status, kindToExclude }, query, } = req; const { files: results, total } = await fileService.asCurrentUser().find({ kind: toArrayOrUndefined(kind), + kindToExclude: toArrayOrUndefined(kindToExclude), name: toArrayOrUndefined(name), status: toArrayOrUndefined(status), extension: toArrayOrUndefined(extension), diff --git a/src/plugins/files_management/public/app.tsx b/src/plugins/files_management/public/app.tsx index becdd05fa0e2ca..3ee4e5f52720c8 100644 --- a/src/plugins/files_management/public/app.tsx +++ b/src/plugins/files_management/public/app.tsx @@ -12,20 +12,33 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { TableListView, UserContentCommonSchema } from '@kbn/content-management-table-list'; import numeral from '@elastic/numeral'; import type { FileJSON } from '@kbn/files-plugin/common'; + import { useFilesManagementContext } from './context'; import { i18nTexts } from './i18n_texts'; import { EmptyPrompt, DiagnosticsFlyout, FileFlyout } from './components'; -type FilesUserContentSchema = UserContentCommonSchema; +type FilesUserContentSchema = Omit & { + attributes: { + title: string; + description?: string; + fileKind: string; + }; +}; function naivelyFuzzify(query: string): string { return query.includes('*') ? query : `*${query}*`; } export const App: FunctionComponent = () => { - const { filesClient } = useFilesManagementContext(); + const { filesClient, getFileKindDefinition, getAllFindKindDefinitions } = + useFilesManagementContext(); const [showDiagnosticsFlyout, setShowDiagnosticsFlyout] = useState(false); const [selectedFile, setSelectedFile] = useState(undefined); + + const kindToExcludeFromSearch = getAllFindKindDefinitions() + .filter(({ managementUiActions }) => managementUiActions?.list?.enabled === false) + .map(({ id }) => id); + return (
@@ -37,7 +50,10 @@ export const App: FunctionComponent = () => { entityNamePlural={i18nTexts.entityNamePlural} findItems={(searchQuery) => filesClient - .find({ name: searchQuery ? naivelyFuzzify(searchQuery) : undefined }) + .find({ + name: searchQuery ? naivelyFuzzify(searchQuery) : undefined, + kindToExclude: kindToExcludeFromSearch, + }) .then(({ files, total }) => ({ hits: files.map((file) => ({ id: file.id, @@ -71,6 +87,12 @@ export const App: FunctionComponent = () => { {i18nTexts.diagnosticsFlyoutTitle} , ]} + rowItemActions={({ attributes }) => { + const definition = getFileKindDefinition(attributes.fileKind); + return { + delete: definition?.managementUiActions?.delete, + }; + }} /> {showDiagnosticsFlyout && ( setShowDiagnosticsFlyout(false)} /> diff --git a/src/plugins/files_management/public/context.tsx b/src/plugins/files_management/public/context.tsx index 18f031b84e5c1e..0688c5a7edecbc 100644 --- a/src/plugins/files_management/public/context.tsx +++ b/src/plugins/files_management/public/context.tsx @@ -12,9 +12,16 @@ import type { AppContext } from './types'; const FilesManagementAppContext = createContext(null as unknown as AppContext); -export const FilesManagementAppContextProvider: FC = ({ children, filesClient }) => { +export const FilesManagementAppContextProvider: FC = ({ + children, + filesClient, + getFileKindDefinition, + getAllFindKindDefinitions, +}) => { return ( - + {children} ); diff --git a/src/plugins/files_management/public/i18n_texts.ts b/src/plugins/files_management/public/i18n_texts.ts index c5f4956af372f0..d430038dcdddcd 100644 --- a/src/plugins/files_management/public/i18n_texts.ts +++ b/src/plugins/files_management/public/i18n_texts.ts @@ -101,4 +101,7 @@ export const i18nTexts = { defaultMessage: 'Upload error', }), } as Record, + rowCheckboxDisabled: i18n.translate('filesManagement.table.checkBoxDisabledLabel', { + defaultMessage: 'This file cannot be deleted.', + }), }; diff --git a/src/plugins/files_management/public/mount_management_section.tsx b/src/plugins/files_management/public/mount_management_section.tsx index 7dce1986237a7f..9c7091516d46e0 100755 --- a/src/plugins/files_management/public/mount_management_section.tsx +++ b/src/plugins/files_management/public/mount_management_section.tsx @@ -30,6 +30,10 @@ export const mountManagementSection = ( startDeps: StartDependencies, { element, history }: ManagementAppMountParams ) => { + const { + files: { filesClientFactory, getAllFindKindDefinitions, getFileKindDefinition }, + } = startDeps; + ReactDOM.render( @@ -41,7 +45,9 @@ export const mountManagementSection = ( }} > diff --git a/src/plugins/files_management/public/types.ts b/src/plugins/files_management/public/types.ts index 2a73b69bea0173..303d5e1c5d1a72 100755 --- a/src/plugins/files_management/public/types.ts +++ b/src/plugins/files_management/public/types.ts @@ -11,6 +11,8 @@ import { ManagementSetup } from '@kbn/management-plugin/public'; export interface AppContext { filesClient: FilesClient; + getFileKindDefinition: FilesStart['getFileKindDefinition']; + getAllFindKindDefinitions: FilesStart['getAllFindKindDefinitions']; } export interface SetupDependencies { From e951205f7e488ce0d2f53bae504c7f014a3bece3 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 24 Apr 2023 12:00:32 -0400 Subject: [PATCH 54/65] [Security Solution][Endpoint] Define possible `execute` response failure codes for use in the UI (#155316) ## Summary - Updates the list of known Response Actions response codes with codes for the `execute` response action --- .../endpoint_action_generator.ts | 65 ++++++------- .../common/endpoint/types/actions.ts | 1 + .../integration_tests/execute_action.test.tsx | 34 +++++++ .../lib/endpoint_action_response_codes.ts | 91 +++++++++++++++++++ .../response_actions_log.test.tsx | 31 +++---- 5 files changed, 173 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts index 855dd3a3fe439b..fdf75da0a134ec 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts @@ -155,38 +155,37 @@ export class EndpointActionGenerator extends BaseDataGenerator { TOutputType extends object = object, TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes >( - overrides: Partial> = {} + overrides: DeepPartial> = {} ): ActionDetails { - const details: ActionDetails = merge( - { - agents: ['agent-a'], - command: 'isolate', - completedAt: '2022-04-30T16:08:47.449Z', - hosts: { 'agent-a': { name: 'Host-agent-a' } }, - id: '123', - isCompleted: true, - isExpired: false, - wasSuccessful: true, - errors: undefined, - startedAt: '2022-04-27T16:08:47.449Z', - status: 'successful', - comment: 'thisisacomment', - createdBy: 'auserid', - parameters: undefined, - outputs: {}, - agentState: { - 'agent-a': { - errors: undefined, - isCompleted: true, - completedAt: '2022-04-30T16:08:47.449Z', - wasSuccessful: true, - }, + const details: ActionDetails = { + agents: ['agent-a'], + command: 'isolate', + completedAt: '2022-04-30T16:08:47.449Z', + hosts: { 'agent-a': { name: 'Host-agent-a' } }, + id: '123', + isCompleted: true, + isExpired: false, + wasSuccessful: true, + errors: undefined, + startedAt: '2022-04-27T16:08:47.449Z', + status: 'successful', + comment: 'thisisacomment', + createdBy: 'auserid', + parameters: undefined, + outputs: {}, + agentState: { + 'agent-a': { + errors: undefined, + isCompleted: true, + completedAt: '2022-04-30T16:08:47.449Z', + wasSuccessful: true, }, }, - overrides - ); + }; - if (details.command === 'get-file') { + const command = overrides.command ?? details.command; + + if (command === 'get-file') { if (!details.parameters) { ( details as ActionDetails< @@ -213,7 +212,7 @@ export class EndpointActionGenerator extends BaseDataGenerator { } } - if (details.command === 'execute') { + if (command === 'execute') { if (!details.parameters) { ( details as ActionDetails< @@ -233,14 +232,17 @@ export class EndpointActionGenerator extends BaseDataGenerator { [details.agents[0]]: this.generateExecuteActionResponseOutput({ content: { output_file_id: getFileDownloadId(details, details.agents[0]), - ...overrides.outputs?.[details.agents[0]].content, + ...(overrides.outputs?.[details.agents[0]]?.content ?? {}), }, }), }; } } - return details as unknown as ActionDetails; + return merge(details, overrides as ActionDetails) as unknown as ActionDetails< + TOutputType, + TParameters + >; } randomGetFileFailureCode(): string { @@ -310,6 +312,7 @@ export class EndpointActionGenerator extends BaseDataGenerator { { type: 'json', content: { + code: 'ra_execute_success_done', stdout: this.randomChoice([ this.randomString(1280), this.randomString(3580), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index d9082f8aa1d8c9..f8f5da28943b83 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -65,6 +65,7 @@ export interface ResponseActionGetFileOutputContent { } export interface ResponseActionExecuteOutputContent { + code: string; /* The truncated 'tail' output of the command */ stdout: string; /* The truncated 'tail' of any errors generated by the command */ diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx index c3f94871c8f674..8d5d8907924f6d 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx @@ -23,6 +23,8 @@ import { getEndpointAuthzInitialStateMock } from '../../../../../../common/endpo import type { EndpointPrivileges } from '../../../../../../common/endpoint/types'; import { INSUFFICIENT_PRIVILEGES_FOR_COMMAND } from '../../../../../common/translations'; import type { HttpFetchOptionsWithPath } from '@kbn/core-http-browser'; +import { endpointActionResponseCodes } from '../../lib/endpoint_action_response_codes'; +import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator'; jest.mock('../../../../../common/components/user_privileges'); jest.mock('../../../../../common/experimental_features_service'); @@ -180,4 +182,36 @@ describe('When using execute action from response actions console', () => { ); }); }); + + it.each( + Object.keys(endpointActionResponseCodes).filter((key) => key.startsWith('ra_execute_error')) + )('should display known error message for response failure: %s', async (errorCode) => { + apiMocks.responseProvider.actionDetails.mockReturnValue({ + data: new EndpointActionGenerator('seed').generateActionDetails({ + command: 'execute', + errors: ['some error happen in endpoint'], + wasSuccessful: false, + outputs: { + 'agent-a': { + content: { + code: errorCode, + }, + }, + }, + }), + }); + + const { getByTestId } = await render(); + enterConsoleCommand(renderResult, 'execute --command="ls -l"'); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalled(); + }); + + await waitFor(() => { + expect(getByTestId('execute-actionFailure')).toHaveTextContent( + endpointActionResponseCodes[errorCode] + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts index 746b9201cd2008..c2595c625b7faa 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts @@ -123,6 +123,97 @@ const CODES = Object.freeze({ 'xpack.securitySolution.endpointActionResponseCodes.killProcess.notPermittedSuccess', { defaultMessage: 'The provided process cannot be killed' } ), + + // ----------------------------------------------------------------- + // EXECUTE CODES + // ----------------------------------------------------------------- + + // Dev: + // Something interrupted preparing the zip: file read error, zip error. I think these should be rare, + // and should succeed on retry by the user or result in file-not-found. We might implement some retries + // internally but I'm leaning to the opinion that we should rather quickly send the feedback to the + // user to let them decide. + ra_execute_error_processing: i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingError', + { + defaultMessage: 'Unable to create execution output zip file.', + } + ), + + // Dev: + // Executing timeout has been reached, the command was killed. + 'ra_execute_error_processing-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingTimeout', + { defaultMessage: 'Command execution was terminated. It exceeded the provided timeout.' } + ), + + // Dev: + // Execution was interrupted, for example: system shutdown, endpoint service stop/restart. + 'ra_execute_error_processing-interrupted': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingInterrupted', + { + defaultMessage: 'Command execution was absolutely interrupted.', + } + ), + + // Dev: + // Too many active execute actions, limit 10. Execute actions are allowed to run in parallel, we must + // take into account resource use impact on endpoint as customers are piky about CPU/MEM utilization. + 'ra_execute_error_to-many-requests': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.toManyRequests', + { + defaultMessage: 'Too many concurrent command execution actions.', + } + ), + + // Dev: + // generic failure (rare corner case, software bug, etc) + ra_execute_error_failure: i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.failure', + { defaultMessage: 'Unknown failure while executing command.' } + ), + + // Dev: + // Max pending response zip uploads has been reached, limit 10. Endpoint can't use unlimited disk space. + 'ra_execute_error_disk-quota': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.diskQuotaError', + { + defaultMessage: 'Too many pending command execution output zip files.', + } + ), + + // Dev: + // The fleet upload API was unreachable (not just busy). This may mean policy misconfiguration, in which + // case health status in Kibana should indicate degraded, or maybe network configuration problems, or fleet + // server problems HTTP 500. This excludes offline status, where endpoint should just wait for network connection. + 'ra_execute_error_upload-api-unreachable': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.uploadApiUnreachable', + { + defaultMessage: + 'Failed to upload command execution output zip file. Unable to reach Fleet Server upload API.', + } + ), + + // Dev: + // Perhaps internet connection was too slow or unstable to upload all chunks before unique + // upload-id expired. Endpoint will re-try a bit, max 3 times. + 'ra_execute_error_upload-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.outputUploadTimeout', + { + defaultMessage: 'Failed to upload command execution output zip file. Upload timed out', + } + ), + + // DEV: + // Upload API could be busy, endpoint should periodically re-try (2 days = 192 x 15min, assuming + // that with 1Mbps 15min is enough to upload 100MB) + 'ra_execute_error_queue-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.queueTimeout', + { + defaultMessage: + 'Failed to upload command execution output zip file. Timed out while queued waiting for Fleet Server', + } + ), }); /** diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx index 6360b55b1c06f9..6fc62dd6cf8519 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx @@ -652,10 +652,14 @@ describe('Response actions history', () => { }); it('should contain expected output accordions for `execute` action WITH execute operation privilege', async () => { - const actionDetails = await getActionListMock({ actionCount: 1, commands: ['execute'] }); + const actionListApiResponse = await getActionListMock({ + actionCount: 1, + agentIds: ['agent-a'], + commands: ['execute'], + }); useGetEndpointActionListMock.mockReturnValue({ ...getBaseMockedActionList(), - data: actionDetails, + data: actionListApiResponse, }); mockUseGetFileInfo = { @@ -669,17 +673,7 @@ describe('Response actions history', () => { isFetched: true, error: null, data: { - data: { - ...apiMocks.responseProvider.actionDetails({ - path: `/api/endpoint/action/${actionDetails.data[0].id}`, - }).data, - outputs: { - [actionDetails.data[0].agents[0]]: { - content: {}, - type: 'json', - }, - }, - }, + data: actionListApiResponse.data[0], }, }; @@ -714,7 +708,11 @@ describe('Response actions history', () => { }); useGetEndpointActionListMock.mockReturnValue({ ...getBaseMockedActionList(), - data: await getActionListMock({ actionCount: 1, commands: ['execute'] }), + data: await getActionListMock({ + actionCount: 1, + commands: ['execute'], + agentIds: ['agent-a'], + }), }); render(); @@ -723,10 +721,7 @@ describe('Response actions history', () => { const expandButton = getByTestId(`${testPrefix}-expand-button`); userEvent.click(expandButton); - const executeAccordions = getByTestId( - `${testPrefix}-actionsLogTray-executeResponseOutput-output` - ); - expect(executeAccordions).toBeTruthy(); + expect(getByTestId(`${testPrefix}-actionsLogTray-executeResponseOutput-output`)); }); it('should not contain full output download link in expanded row for `execute` action WITHOUT Actions Log privileges', async () => { From 50e3ff2f23ea38396c58961d4282674b9e2da0dd Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 24 Apr 2023 18:17:23 +0200 Subject: [PATCH 55/65] [Synthetics] Add license issued_to value in request (#155600) --- .../service_api_client.test.ts | 40 +++++++++++++++---- .../synthetics_service/service_api_client.ts | 12 +++--- .../synthetics_service/synthetics_service.ts | 12 +++--- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts index 22ec8e8a3213e0..2ed86eb91ae52e 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts @@ -14,6 +14,24 @@ import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { ServiceConfig } from '../../common/config'; import axios from 'axios'; import { LocationStatus, PublicLocations } from '../../common/runtime_types'; +import { LicenseGetResponse } from '@elastic/elasticsearch/lib/api/types'; + +const licenseMock: LicenseGetResponse = { + license: { + status: 'active', + uid: '1d34eb9f-e66f-47d1-8d24-cd60d187587a', + type: 'trial', + issue_date: '2022-05-05T14:25:00.732Z', + issue_date_in_millis: 165176070074432, + expiry_date: '2022-06-04T14:25:00.732Z', + expiry_date_in_millis: 165435270073332, + max_nodes: 1000, + max_resource_units: null, + issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', + issuer: 'elasticsearch', + start_date_in_millis: -1, + }, +}; jest.mock('axios', () => jest.fn()); jest.mock('@kbn/server-http-tools', () => ({ @@ -167,7 +185,7 @@ describe('callAPI', () => { await apiClient.callAPI('POST', { monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(spy).toHaveBeenCalledTimes(3); @@ -181,7 +199,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', devUrl @@ -195,7 +213,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central_qa') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', 'https://qa.service.elstc.co' @@ -209,7 +227,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central_staging') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', 'https://qa.service.stg.co' @@ -223,6 +241,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -242,6 +261,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -261,6 +281,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -324,7 +345,7 @@ describe('callAPI', () => { await apiClient.callAPI('POST', { monitors: testMonitors, output, - licenseLevel: 'platinum', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -333,7 +354,8 @@ describe('callAPI', () => { is_edit: undefined, output, stack_version: '8.7.0', - license_level: 'platinum', + license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', @@ -376,7 +398,7 @@ describe('callAPI', () => { await apiClient.runOnce({ monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -386,6 +408,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', @@ -428,7 +451,7 @@ describe('callAPI', () => { await apiClient.syncMonitors({ monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -438,6 +461,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts index bfc872f3680a88..4fea1d04b64e4f 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts @@ -11,6 +11,7 @@ import { catchError, tap } from 'rxjs/operators'; import * as https from 'https'; import { SslConfig } from '@kbn/server-http-tools'; import { Logger } from '@kbn/core/server'; +import { LicenseGetLicenseInformation } from '@elastic/elasticsearch/lib/api/types'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { sendErrorTelemetryEvents } from '../routes/telemetry/monitor_upgrade_sender'; import { MonitorFields, PublicLocations, ServiceLocationErrors } from '../../common/runtime_types'; @@ -27,7 +28,7 @@ export interface ServiceData { }; endpoint?: 'monitors' | 'runOnce' | 'sync'; isEdit?: boolean; - licenseLevel: string; + license: LicenseGetLicenseInformation; } export class ServiceAPIClient { @@ -142,7 +143,7 @@ export class ServiceAPIClient { async callAPI( method: 'POST' | 'PUT' | 'DELETE', - { monitors: allMonitors, output, endpoint, isEdit, licenseLevel }: ServiceData + { monitors: allMonitors, output, endpoint, isEdit, license }: ServiceData ) { if (this.username === TEST_SERVICE_USERNAME) { // we don't want to call service while local integration tests are running @@ -159,7 +160,7 @@ export class ServiceAPIClient { ); if (locMonitors.length > 0) { const promise = this.callServiceEndpoint( - { monitors: locMonitors, isEdit, endpoint, output, licenseLevel }, + { monitors: locMonitors, isEdit, endpoint, output, license }, method, url ); @@ -200,7 +201,7 @@ export class ServiceAPIClient { } async callServiceEndpoint( - { monitors, output, endpoint = 'monitors', isEdit, licenseLevel }: ServiceData, + { monitors, output, endpoint = 'monitors', isEdit, license }: ServiceData, method: 'POST' | 'PUT' | 'DELETE', baseUrl: string ) { @@ -233,7 +234,8 @@ export class ServiceAPIClient { output, stack_version: this.stackVersion, is_edit: isEdit, - license_level: licenseLevel, + license_level: license.type, + license_issued_to: license.issued_to, }, headers: authHeader, httpsAgent: this.getHttpsAgent(baseUrl), diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index bd9c0032984c33..e4ef7cbbb6e67c 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -307,7 +307,7 @@ export class SyntheticsService { this.syncErrors = await this.apiClient.post({ monitors, output, - licenseLevel: license.type, + license, }); } return this.syncErrors; @@ -329,7 +329,7 @@ export class SyntheticsService { monitors, output, isEdit, - licenseLevel: license.type, + license, }; this.syncErrors = await this.apiClient.put(data); @@ -372,7 +372,7 @@ export class SyntheticsService { service.syncErrors = await this.apiClient.syncMonitors({ monitors, output, - licenseLevel: license.type, + license, }); } catch (e) { sendErrorTelemetryEvents(service.logger, service.server.telemetry, { @@ -406,7 +406,7 @@ export class SyntheticsService { return await this.apiClient.runOnce({ monitors, output, - licenseLevel: license.type, + license, }); } catch (e) { this.logger.error(e); @@ -429,7 +429,7 @@ export class SyntheticsService { const data = { output, monitors: this.formatConfigs(configs), - licenseLevel: license.type, + license, }; return await this.apiClient.delete(data); } @@ -453,7 +453,7 @@ export class SyntheticsService { const data = { output, monitors, - licenseLevel: license.type, + license, }; return await this.apiClient.delete(data); } From 3e94b43aa28f12538547217a0eeec224e7fa4263 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Apr 2023 18:47:44 +0200 Subject: [PATCH 56/65] [ML] AIOps: Link from Explain Log Rate Spikes to Log Pattern Analysis (#155121) Adds table actions to Explain Log Rate Spikes to be able to drill down to Log Pattern Analysis. --- .../public/application/utils/url_state.ts | 45 +++++ .../explain_log_rate_spikes_app_state.tsx | 30 ---- .../explain_log_rate_spikes_page.tsx | 10 +- .../category_table/category_table.tsx | 4 +- .../log_categorization_page.tsx | 22 ++- .../log_categorization/use_discover_links.ts | 2 +- .../spike_analysis_table.tsx | 4 +- .../spike_analysis_table_groups.tsx | 4 +- .../table_action_button.tsx | 62 +++++++ .../use_copy_to_clipboard_action.test.tsx | 26 ++- .../use_copy_to_clipboard_action.tsx | 20 ++- .../use_view_in_discover_action.tsx | 36 ++-- ...se_view_in_log_pattern_analysis_action.tsx | 108 ++++++++++++ x-pack/plugins/aiops/public/hooks/use_data.ts | 2 +- x-pack/plugins/aiops/tsconfig.json | 1 + .../apps/aiops/explain_log_rate_spikes.ts | 158 +++++++++++------- .../test/functional/apps/aiops/test_data.ts | 106 ++++++++++++ x-pack/test/functional/apps/aiops/types.ts | 12 +- ...n_log_rate_spikes_analysis_groups_table.ts | 50 ++++++ .../explain_log_rate_spikes_data_generator.ts | 8 + .../aiops/explain_log_rate_spikes_page.ts | 8 +- .../test/functional/services/aiops/index.ts | 3 + .../aiops/log_pattern_analysis_page.ts | 42 +++++ 23 files changed, 631 insertions(+), 132 deletions(-) create mode 100644 x-pack/plugins/aiops/public/application/utils/url_state.ts create mode 100644 x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx create mode 100644 x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx create mode 100644 x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts diff --git a/x-pack/plugins/aiops/public/application/utils/url_state.ts b/x-pack/plugins/aiops/public/application/utils/url_state.ts new file mode 100644 index 00000000000000..9fdaa443f4c751 --- /dev/null +++ b/x-pack/plugins/aiops/public/application/utils/url_state.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { Filter, Query } from '@kbn/es-query'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + +import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from './search_utils'; + +const defaultSearchQuery = { + match_all: {}, +}; + +export interface AiOpsPageUrlState { + pageKey: 'AIOPS_INDEX_VIEWER'; + pageUrlState: AiOpsIndexBasedAppState; +} + +export interface AiOpsIndexBasedAppState { + searchString?: Query['query']; + searchQuery?: estypes.QueryDslQueryContainer; + searchQueryLanguage: SearchQueryLanguage; + filters?: Filter[]; +} + +export type AiOpsFullIndexBasedAppState = Required; + +export const getDefaultAiOpsListState = ( + overrides?: Partial +): AiOpsFullIndexBasedAppState => ({ + searchString: '', + searchQuery: defaultSearchQuery, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + filters: [], + ...overrides, +}); + +export const isFullAiOpsListState = (arg: unknown): arg is AiOpsFullIndexBasedAppState => { + return isPopulatedObject(arg, Object.keys(getDefaultAiOpsListState())); +}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx index ce69ea9fea3ae8..b30e3a7d1e6fbb 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx @@ -10,7 +10,6 @@ import { pick } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; -import type { Filter, Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import type { SavedSearch } from '@kbn/discover-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -21,7 +20,6 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; -import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../application/utils/search_utils'; import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context'; import { AiopsAppContext } from '../../hooks/use_aiops_app_context'; import { DataSourceContext } from '../../hooks/use_data_source'; @@ -42,34 +40,6 @@ export interface ExplainLogRateSpikesAppStateProps { appDependencies: AiopsAppDependencies; } -const defaultSearchQuery = { - match_all: {}, -}; - -export interface AiOpsPageUrlState { - pageKey: 'AIOPS_INDEX_VIEWER'; - pageUrlState: AiOpsIndexBasedAppState; -} - -export interface AiOpsIndexBasedAppState { - searchString?: Query['query']; - searchQuery?: Query['query']; - searchQueryLanguage: SearchQueryLanguage; - filters?: Filter[]; -} - -export const getDefaultAiOpsListState = ( - overrides?: Partial -): Required => ({ - searchString: '', - searchQuery: defaultSearchQuery, - searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, - filters: [], - ...overrides, -}); - -export const restorableDefaults = getDefaultAiOpsListState(); - export const ExplainLogRateSpikesAppState: FC = ({ dataView, savedSearch, diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx index 80640f59901bc8..fb9ce01c633918 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useState, FC } from 'react'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiEmptyPrompt, EuiFlexGroup, @@ -28,6 +29,10 @@ import { useDataSource } from '../../hooks/use_data_source'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { SearchQueryLanguage } from '../../application/utils/search_utils'; import { useData } from '../../hooks/use_data'; +import { + getDefaultAiOpsListState, + type AiOpsPageUrlState, +} from '../../application/utils/url_state'; import { DocumentCountContent } from '../document_count_content/document_count_content'; import { SearchPanel } from '../search_panel'; @@ -35,7 +40,6 @@ import type { GroupTableItem } from '../spike_analysis_table/types'; import { useSpikeAnalysisTableRowContext } from '../spike_analysis_table/spike_analysis_table_row_provider'; import { PageHeader } from '../page_header'; -import { restorableDefaults, type AiOpsPageUrlState } from './explain_log_rate_spikes_app_state'; import { ExplainLogRateSpikesAnalysis } from './explain_log_rate_spikes_analysis'; function getDocumentCountStatsSplitLabel( @@ -66,7 +70,7 @@ export const ExplainLogRateSpikesPage: FC = () => { const [aiopsListState, setAiopsListState] = usePageUrlState( 'AIOPS_INDEX_VIEWER', - restorableDefaults + getDefaultAiOpsListState() ); const [globalState, setGlobalState] = useUrlState('_g'); @@ -80,7 +84,7 @@ export const ExplainLogRateSpikesPage: FC = () => { const setSearchParams = useCallback( (searchParams: { - searchQuery: Query['query']; + searchQuery: estypes.QueryDslQueryContainer; searchString: Query['query']; queryLanguage: SearchQueryLanguage; filters: Filter[]; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx index c888694a7b0c3b..747f90d5423541 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx @@ -25,7 +25,7 @@ import { import { useDiscoverLinks } from '../use_discover_links'; import { MiniHistogram } from '../../mini_histogram'; import { useEuiTheme } from '../../../hooks/use_eui_theme'; -import type { AiOpsIndexBasedAppState } from '../../explain_log_rate_spikes/explain_log_rate_spikes_app_state'; +import type { AiOpsFullIndexBasedAppState } from '../../../application/utils/url_state'; import type { EventRate, Category, SparkLinesPerCategory } from '../use_categorize_request'; import { useTableState } from './use_table_state'; @@ -42,7 +42,7 @@ interface Props { dataViewId: string; selectedField: string | undefined; timefilter: TimefilterContract; - aiopsListState: Required; + aiopsListState: AiOpsFullIndexBasedAppState; pinnedCategory: Category | null; setPinnedCategory: (category: Category | null) => void; selectedCategory: Category | null; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index b673d8a498a213..7c7d0001aea428 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -4,8 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiButton, EuiSpacer, @@ -21,14 +23,18 @@ import { import { Filter, Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useUrlState } from '@kbn/ml-url-state'; +import { usePageUrlState, useUrlState } from '@kbn/ml-url-state'; import { useDataSource } from '../../hooks/use_data_source'; import { useData } from '../../hooks/use_data'; import type { SearchQueryLanguage } from '../../application/utils/search_utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; +import { + getDefaultAiOpsListState, + isFullAiOpsListState, + type AiOpsPageUrlState, +} from '../../application/utils/url_state'; -import { restorableDefaults } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state'; import { SearchPanel } from '../search_panel'; import { PageHeader } from '../page_header'; @@ -47,7 +53,10 @@ export const LogCategorizationPage: FC = () => { const { dataView, savedSearch } = useDataSource(); const { runCategorizeRequest, cancelRequest } = useCategorizeRequest(); - const [aiopsListState, setAiopsListState] = useState(restorableDefaults); + const [aiopsListState, setAiopsListState] = usePageUrlState( + 'AIOPS_INDEX_VIEWER', + getDefaultAiOpsListState() + ); const [globalState, setGlobalState] = useUrlState('_g'); const [selectedField, setSelectedField] = useState(); const [selectedCategory, setSelectedCategory] = useState(null); @@ -76,7 +85,7 @@ export const LogCategorizationPage: FC = () => { const setSearchParams = useCallback( (searchParams: { - searchQuery: Query['query']; + searchQuery: estypes.QueryDslQueryContainer; searchString: Query['query']; queryLanguage: SearchQueryLanguage; filters: Filter[]; @@ -289,7 +298,10 @@ export const LogCategorizationPage: FC = () => { fieldSelected={selectedField !== null} /> - {selectedField !== undefined && categories !== null && categories.length > 0 ? ( + {selectedField !== undefined && + categories !== null && + categories.length > 0 && + isFullAiOpsListState(aiopsListState) ? ( = ({ const copyToClipBoardAction = useCopyToClipboardAction(); const viewInDiscoverAction = useViewInDiscoverAction(dataViewId); + const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId); const columns: Array> = [ { @@ -238,7 +240,7 @@ export const SpikeAnalysisTable: FC = ({ name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', { defaultMessage: 'Actions', }), - actions: [viewInDiscoverAction, copyToClipBoardAction], + actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction], width: ACTIONS_COLUMN_WIDTH, valign: 'middle', }, diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index b319db0088d4dc..ca55b43907bd69 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -40,6 +40,7 @@ import { useSpikeAnalysisTableRowContext } from './spike_analysis_table_row_prov import type { GroupTableItem } from './types'; import { useCopyToClipboardAction } from './use_copy_to_clipboard_action'; import { useViewInDiscoverAction } from './use_view_in_discover_action'; +import { useViewInLogPatternAnalysisAction } from './use_view_in_log_pattern_analysis_action'; const NARROW_COLUMN_WIDTH = '120px'; const EXPAND_COLUMN_WIDTH = '40px'; @@ -121,6 +122,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ const copyToClipBoardAction = useCopyToClipboardAction(); const viewInDiscoverAction = useViewInDiscoverAction(dataViewId); + const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId); const columns: Array> = [ { @@ -355,7 +357,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', { defaultMessage: 'Actions', }), - actions: [viewInDiscoverAction, copyToClipBoardAction], + actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction], width: ACTIONS_COLUMN_WIDTH, valign: 'top', }, diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx new file mode 100644 index 00000000000000..16c91f8d3851fe --- /dev/null +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { type FC } from 'react'; + +import { EuiLink, EuiIcon, EuiText, EuiToolTip, type IconType } from '@elastic/eui'; + +interface TableActionButtonProps { + iconType: IconType; + dataTestSubjPostfix: string; + isDisabled: boolean; + label: string; + tooltipText?: string; + onClick: () => void; +} + +export const TableActionButton: FC = ({ + iconType, + dataTestSubjPostfix, + isDisabled, + label, + tooltipText, + onClick, +}) => { + const buttonContent = ( + <> + + {label} + + ); + + const unwrappedButton = !isDisabled ? ( + + {buttonContent} + + ) : ( + + {buttonContent} + + ); + + if (tooltipText) { + return {unwrappedButton}; + } + + return unwrappedButton; +}; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx index 82359b3d2b1aa5..0984c76a4b1708 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx @@ -30,11 +30,19 @@ describe('useCopyToClipboardAction', () => { it('renders the action for a single significant term', async () => { execCommandMock.mockImplementationOnce(() => true); const { result } = renderHook(() => useCopyToClipboardAction()); - const { getByLabelText } = render((result.current as Action).render(significantTerms[0])); + const { findByText, getByTestId } = render( + (result.current as Action).render(significantTerms[0]) + ); - const button = getByLabelText('Copy field/value pair as KQL syntax to clipboard'); + const button = getByTestId('aiopsTableActionButtonCopyToClipboard enabled'); - expect(button).toBeInTheDocument(); + userEvent.hover(button); + + // The tooltip from EUI takes 250ms to appear, so we must + // use a `find*` query to asynchronously poll for it. + expect( + await findByText('Copy field/value pair as KQL syntax to clipboard') + ).toBeInTheDocument(); await act(async () => { await userEvent.click(button); @@ -50,12 +58,16 @@ describe('useCopyToClipboardAction', () => { it('renders the action for a group of items', async () => { execCommandMock.mockImplementationOnce(() => true); const groupTableItems = getGroupTableItems(finalSignificantTermGroups); - const { result } = renderHook(() => useCopyToClipboardAction()); - const { getByLabelText } = render((result.current as Action).render(groupTableItems[0])); + const { result } = renderHook(useCopyToClipboardAction); + const { findByText, getByText } = render((result.current as Action).render(groupTableItems[0])); + + const button = getByText('Copy to clipboard'); - const button = getByLabelText('Copy group items as KQL syntax to clipboard'); + userEvent.hover(button); - expect(button).toBeInTheDocument(); + // The tooltip from EUI takes 250ms to appear, so we must + // use a `find*` query to asynchronously poll for it. + expect(await findByText('Copy group items as KQL syntax to clipboard')).toBeInTheDocument(); await act(async () => { await userEvent.click(button); diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx index e9924307c1e273..1b906eb56e9886 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx @@ -7,14 +7,22 @@ import React from 'react'; -import { EuiCopy, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { EuiCopy, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isSignificantTerm, type SignificantTerm } from '@kbn/ml-agg-utils'; +import { TableActionButton } from './table_action_button'; import { getTableItemAsKQL } from './get_table_item_as_kql'; import type { GroupTableItem, TableItemAction } from './types'; +const copyToClipboardButtonLabel = i18n.translate( + 'xpack.aiops.spikeAnalysisTable.linksMenu.copyToClipboardButtonLabel', + { + defaultMessage: 'Copy to clipboard', + } +); + const copyToClipboardSignificantTermMessage = i18n.translate( 'xpack.aiops.spikeAnalysisTable.linksMenu.copyToClipboardSignificantTermMessage', { @@ -37,7 +45,15 @@ export const useCopyToClipboardAction = (): TableItemAction => ({ return ( - {(copy) => } + {(copy) => ( + + )} ); diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx index 5f30abb2f6cec9..bd7741bb452bff 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx @@ -7,14 +7,13 @@ import React, { useMemo } from 'react'; -import { EuiIcon, EuiToolTip } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; import type { SignificantTerm } from '@kbn/ml-agg-utils'; import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; +import { TableActionButton } from './table_action_button'; import { getTableItemAsKQL } from './get_table_item_as_kql'; import type { GroupTableItem, TableItemAction } from './types'; @@ -83,19 +82,26 @@ export const useViewInDiscoverAction = (dataViewId?: string): TableItemAction => }; return { - name: () => ( - - - - ), - description: viewInDiscoverMessage, - type: 'button', - onClick: async (tableItem) => { - const openInDiscoverUrl = await generateDiscoverUrl(tableItem); - if (typeof openInDiscoverUrl === 'string') { - await application.navigateToUrl(openInDiscoverUrl); - } + render: (tableItem: SignificantTerm | GroupTableItem) => { + const tooltipText = discoverUrlError ? discoverUrlError : viewInDiscoverMessage; + + const clickHandler = async () => { + const openInDiscoverUrl = await generateDiscoverUrl(tableItem); + if (typeof openInDiscoverUrl === 'string') { + await application.navigateToUrl(openInDiscoverUrl); + } + }; + + return ( + + ); }, - enabled: () => discoverUrlError === undefined, }; }; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx new file mode 100644 index 00000000000000..9388cf147c8ff3 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { SerializableRecord } from '@kbn/utility-types'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import type { SignificantTerm } from '@kbn/ml-agg-utils'; + +import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils'; +import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; + +import { TableActionButton } from './table_action_button'; +import { getTableItemAsKQL } from './get_table_item_as_kql'; +import type { GroupTableItem, TableItemAction } from './types'; + +const viewInLogPatternAnalysisMessage = i18n.translate( + 'xpack.aiops.spikeAnalysisTable.linksMenu.viewInLogPatternAnalysis', + { + defaultMessage: 'View in Log Pattern Analysis', + } +); + +export const useViewInLogPatternAnalysisAction = (dataViewId?: string): TableItemAction => { + const { application, share, data } = useAiopsAppContext(); + + const mlLocator = useMemo(() => share.url.locators.get('ML_APP_LOCATOR'), [share.url.locators]); + + const generateLogPatternAnalysisUrl = async ( + groupTableItem: GroupTableItem | SignificantTerm + ) => { + if (mlLocator !== undefined) { + const searchString = getTableItemAsKQL(groupTableItem); + const ast = fromKueryExpression(searchString); + const searchQuery = toElasticsearchQuery(ast); + + const appState = { + AIOPS_INDEX_VIEWER: { + filters: data.query.filterManager.getFilters(), + // QueryDslQueryContainer type triggers an error as being + // not working with SerializableRecord, however, it works as expected. + searchQuery: searchQuery as unknown, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + searchString: getTableItemAsKQL(groupTableItem), + }, + } as SerializableRecord; + + return await mlLocator.getUrl({ + page: 'aiops/log_categorization', + pageState: { + index: dataViewId, + timeRange: data.query.timefilter.timefilter.getTime(), + appState, + }, + }); + } + }; + + const logPatternAnalysisUrlError = useMemo(() => { + if (!mlLocator) { + return i18n.translate('xpack.aiops.spikeAnalysisTable.mlLocatorMissingErrorMessage', { + defaultMessage: 'No locator for Log Pattern Analysis detected', + }); + } + if (!dataViewId) { + return i18n.translate( + 'xpack.aiops.spikeAnalysisTable.autoGeneratedLogPatternAnalysisLinkErrorMessage', + { + defaultMessage: + 'Unable to link to Log Pattern Analysis; no data view exists for this index', + } + ); + } + }, [dataViewId, mlLocator]); + + return { + render: (tableItem: SignificantTerm | GroupTableItem) => { + const message = logPatternAnalysisUrlError + ? logPatternAnalysisUrlError + : viewInLogPatternAnalysisMessage; + + const clickHandler = async () => { + const openInLogPatternAnalysisUrl = await generateLogPatternAnalysisUrl(tableItem); + if (typeof openInLogPatternAnalysisUrl === 'string') { + await application.navigateToUrl(openInLogPatternAnalysisUrl); + } + }; + + const isDisabled = logPatternAnalysisUrlError !== undefined; + + return ( + + ); + }, + }; +}; diff --git a/x-pack/plugins/aiops/public/hooks/use_data.ts b/x-pack/plugins/aiops/public/hooks/use_data.ts index c390582ccae1af..62f4c596cc60e6 100644 --- a/x-pack/plugins/aiops/public/hooks/use_data.ts +++ b/x-pack/plugins/aiops/public/hooks/use_data.ts @@ -18,7 +18,7 @@ import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker'; import { PLUGIN_ID } from '../../common'; import type { DocumentStatsSearchStrategyParams } from '../get_document_stats'; -import type { AiOpsIndexBasedAppState } from '../components/explain_log_rate_spikes/explain_log_rate_spikes_app_state'; +import type { AiOpsIndexBasedAppState } from '../application/utils/url_state'; import { getEsQueryFromSavedSearch } from '../application/utils/search_utils'; import type { GroupTableItem } from '../components/spike_analysis_table/types'; diff --git a/x-pack/plugins/aiops/tsconfig.json b/x-pack/plugins/aiops/tsconfig.json index 89c236ba25c992..6c9aafffa26d04 100644 --- a/x-pack/plugins/aiops/tsconfig.json +++ b/x-pack/plugins/aiops/tsconfig.json @@ -50,6 +50,7 @@ "@kbn/ml-route-utils", "@kbn/unified-field-list-plugin", "@kbn/ml-random-sampler-utils", + "@kbn/utility-types", "@kbn/ml-error-utils", ], "exclude": [ diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index 54b1baae454bde..7731eea1d9d7a0 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -13,8 +13,8 @@ import type { FtrProviderContext } from '../../ftr_provider_context'; import { isTestDataExpectedWithSampleProbability, type TestData } from './types'; import { explainLogRateSpikesTestData } from './test_data'; -export default function ({ getPageObject, getService }: FtrProviderContext) { - const headerPage = getPageObject('header'); +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'console', 'header', 'home', 'security']); const elasticChart = getService('elasticChart'); const aiops = getService('aiops'); @@ -58,7 +58,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.assertSamplingProbabilityMissing(); } - await headerPage.waitUntilLoadingHasFinished(); + await PageObjects.header.waitUntilLoadingHasFinished(); await ml.testExecution.logTestStep( `${testData.suiteTitle} displays elements in the doc count panel correctly` @@ -78,77 +78,78 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.clickDocumentCountChart(testData.chartClickCoordinates); await aiops.explainLogRateSpikesPage.assertAnalysisSectionExists(); - await ml.testExecution.logTestStep('displays the no results found prompt'); - await aiops.explainLogRateSpikesPage.assertNoResultsFoundEmptyPromptExists(); + if (testData.brushDeviationTargetTimestamp) { + await ml.testExecution.logTestStep('displays the no results found prompt'); + await aiops.explainLogRateSpikesPage.assertNoResultsFoundEmptyPromptExists(); - await ml.testExecution.logTestStep('adjusts the brushes to get analysis results'); - await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(false); + await ml.testExecution.logTestStep('adjusts the brushes to get analysis results'); + await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(false); - // Get the current width of the deviation brush for later comparison. - const brushSelectionWidthBefore = await aiops.explainLogRateSpikesPage.getBrushSelectionWidth( - 'aiopsBrushDeviation' - ); - - // Get the px values for the timestamp we want to move the brush to. - const { targetPx, intervalPx } = await aiops.explainLogRateSpikesPage.getPxForTimestamp( - testData.brushDeviationTargetTimestamp - ); - - // Adjust the right brush handle - await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushDeviation', - 'handle--e', - targetPx + intervalPx * testData.brushIntervalFactor - ); - - // Adjust the left brush handle - await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushDeviation', - 'handle--w', - targetPx - intervalPx * (testData.brushIntervalFactor - 1) - ); + // Get the current width of the deviation brush for later comparison. + const brushSelectionWidthBefore = + await aiops.explainLogRateSpikesPage.getBrushSelectionWidth('aiopsBrushDeviation'); - if (testData.brushBaselineTargetTimestamp) { // Get the px values for the timestamp we want to move the brush to. - const { targetPx: targetBaselinePx } = - await aiops.explainLogRateSpikesPage.getPxForTimestamp( - testData.brushBaselineTargetTimestamp - ); + const { targetPx, intervalPx } = await aiops.explainLogRateSpikesPage.getPxForTimestamp( + testData.brushDeviationTargetTimestamp + ); // Adjust the right brush handle await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushBaseline', + 'aiopsBrushDeviation', 'handle--e', - targetBaselinePx + intervalPx * testData.brushIntervalFactor + targetPx + intervalPx * testData.brushIntervalFactor ); // Adjust the left brush handle await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushBaseline', + 'aiopsBrushDeviation', 'handle--w', - targetBaselinePx - intervalPx * (testData.brushIntervalFactor - 1) + targetPx - intervalPx * (testData.brushIntervalFactor - 1) ); - } - // Get the new brush selection width for later comparison. - const brushSelectionWidthAfter = await aiops.explainLogRateSpikesPage.getBrushSelectionWidth( - 'aiopsBrushDeviation' - ); + if (testData.brushBaselineTargetTimestamp) { + // Get the px values for the timestamp we want to move the brush to. + const { targetPx: targetBaselinePx } = + await aiops.explainLogRateSpikesPage.getPxForTimestamp( + testData.brushBaselineTargetTimestamp + ); + + // Adjust the right brush handle + await aiops.explainLogRateSpikesPage.adjustBrushHandler( + 'aiopsBrushBaseline', + 'handle--e', + targetBaselinePx + intervalPx * testData.brushIntervalFactor + ); - // Assert the adjusted brush: The selection width should have changed and - // we test if the selection is smaller than two bucket intervals. - // Finally, the adjusted brush should trigger - // a warning on the "Rerun analysis" button. - expect(brushSelectionWidthBefore).not.to.be(brushSelectionWidthAfter); - expect(brushSelectionWidthAfter).not.to.be.greaterThan( - intervalPx * 2 * testData.brushIntervalFactor - ); + // Adjust the left brush handle + await aiops.explainLogRateSpikesPage.adjustBrushHandler( + 'aiopsBrushBaseline', + 'handle--w', + targetBaselinePx - intervalPx * (testData.brushIntervalFactor - 1) + ); + } + + // Get the new brush selection width for later comparison. + const brushSelectionWidthAfter = + await aiops.explainLogRateSpikesPage.getBrushSelectionWidth('aiopsBrushDeviation'); + + // Assert the adjusted brush: The selection width should have changed and + // we test if the selection is smaller than two bucket intervals. + // Finally, the adjusted brush should trigger + // a warning on the "Rerun analysis" button. + expect(brushSelectionWidthBefore).not.to.be(brushSelectionWidthAfter); + expect(brushSelectionWidthAfter).not.to.be.greaterThan( + intervalPx * 2 * testData.brushIntervalFactor + ); - await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(true); + await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(true); - await ml.testExecution.logTestStep('rerun the analysis with adjusted settings'); + await ml.testExecution.logTestStep('rerun the analysis with adjusted settings'); + + await aiops.explainLogRateSpikesPage.clickRerunAnalysisButton(true); + } - await aiops.explainLogRateSpikesPage.clickRerunAnalysisButton(true); await aiops.explainLogRateSpikesPage.assertProgressTitle('Progress: 100% — Done.'); // The group switch should be disabled by default @@ -178,14 +179,14 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); } - // Assert the field selector that allows to costumize grouping + await ml.testExecution.logTestStep('open the field filter'); await aiops.explainLogRateSpikesPage.assertFieldFilterPopoverButtonExists(false); await aiops.explainLogRateSpikesPage.clickFieldFilterPopoverButton(true); await aiops.explainLogRateSpikesPage.assertFieldSelectorFieldNameList( testData.expected.fieldSelectorPopover ); - // Filter fields + await ml.testExecution.logTestStep('filter fields'); await aiops.explainLogRateSpikesPage.setFieldSelectorSearch(testData.fieldSelectorSearch); await aiops.explainLogRateSpikesPage.assertFieldSelectorFieldNameList([ testData.fieldSelectorSearch, @@ -196,6 +197,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); if (testData.fieldSelectorApplyAvailable) { + await ml.testExecution.logTestStep('regroup results'); await aiops.explainLogRateSpikesPage.clickFieldFilterApplyButton(); if (!isTestDataExpectedWithSampleProbability(testData.expected)) { @@ -206,6 +208,28 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); } } + + if (testData.action !== undefined) { + await ml.testExecution.logTestStep('check all table row actions are present'); + await aiops.explainLogRateSpikesAnalysisGroupsTable.assertRowActions( + testData.action.tableRowId + ); + + await ml.testExecution.logTestStep('click log pattern analysis action'); + await aiops.explainLogRateSpikesAnalysisGroupsTable.clickRowAction( + testData.action.tableRowId, + testData.action.type + ); + + await ml.testExecution.logTestStep('check log pattern analysis page loaded correctly'); + await aiops.logPatternAnalysisPageProvider.assertLogCategorizationPageExists(); + await aiops.logPatternAnalysisPageProvider.assertTotalDocumentCount( + testData.action.expected.totalDocCount + ); + await aiops.logPatternAnalysisPageProvider.assertQueryInput( + testData.action.expected.queryBar + ); + } }); } @@ -223,13 +247,27 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await ml.testResources.setKibanaTimeZoneToUTC(); - await ml.securityUI.loginAsMlPowerUser(); + if (testData.dataGenerator === 'kibana_sample_data_logs') { + await PageObjects.security.login('elastic', 'changeme', { + expectSuccess: true, + }); + + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.addSampleDataSet('logs'); + await PageObjects.header.waitUntilLoadingHasFinished(); + } else { + await ml.securityUI.loginAsMlPowerUser(); + } }); after(async () => { await elasticChart.setNewChartUiDebugFlag(false); - await ml.testResources.deleteIndexPatternByTitle(testData.sourceIndexOrSavedSearch); - + if (testData.dataGenerator !== 'kibana_sample_data_logs') { + await ml.testResources.deleteIndexPatternByTitle(testData.sourceIndexOrSavedSearch); + } await aiops.explainLogRateSpikesDataGenerator.removeGeneratedData(testData.dataGenerator); }); diff --git a/x-pack/test/functional/apps/aiops/test_data.ts b/x-pack/test/functional/apps/aiops/test_data.ts index b6d3293aeba81f..95e21fbfc78007 100644 --- a/x-pack/test/functional/apps/aiops/test_data.ts +++ b/x-pack/test/functional/apps/aiops/test_data.ts @@ -7,6 +7,111 @@ import type { TestData } from './types'; +export const kibanaLogsDataViewTestData: TestData = { + suiteTitle: 'kibana sample data logs', + dataGenerator: 'kibana_sample_data_logs', + isSavedSearch: false, + sourceIndexOrSavedSearch: 'kibana_sample_data_logs', + brushIntervalFactor: 1, + chartClickCoordinates: [235, 0], + fieldSelectorSearch: 'referer', + fieldSelectorApplyAvailable: true, + action: { + type: 'LogPatternAnalysis', + tableRowId: '488337254', + expected: { + queryBar: + 'clientip:30.156.16.164 AND host.keyword:elastic-elastic-elastic.org AND ip:30.156.16.163 AND response.keyword:404 AND machine.os.keyword:win xp AND geo.dest:IN AND geo.srcdest:US\\:IN', + totalDocCount: '100', + }, + }, + expected: { + totalDocCountFormatted: '14,074', + analysisGroupsTable: [ + { + group: + '* clientip: 30.156.16.164* host.keyword: elastic-elastic-elastic.org* ip: 30.156.16.163* referer: http://www.elastic-elastic-elastic.com/success/timothy-l-kopra* response.keyword: 404Showing 5 out of 8 items. 8 items unique to this group.', + docCount: '100', + }, + ], + filteredAnalysisGroupsTable: [ + { + group: + '* clientip: 30.156.16.164* host.keyword: elastic-elastic-elastic.org* ip: 30.156.16.163* response.keyword: 404* machine.os.keyword: win xpShowing 5 out of 7 items. 7 items unique to this group.', + docCount: '100', + }, + ], + analysisTable: [ + { + fieldName: 'clientip', + fieldValue: '30.156.16.164', + logRate: 'Chart type:bar chart', + pValue: '3.10e-13', + impact: 'High', + }, + { + fieldName: 'geo.dest', + fieldValue: 'IN', + logRate: 'Chart type:bar chart', + pValue: '0.000716', + impact: 'Medium', + }, + { + fieldName: 'geo.srcdest', + fieldValue: 'US:IN', + logRate: 'Chart type:bar chart', + pValue: '0.000716', + impact: 'Medium', + }, + { + fieldName: 'host.keyword', + fieldValue: 'elastic-elastic-elastic.org', + logRate: 'Chart type:bar chart', + pValue: '7.14e-9', + impact: 'High', + }, + { + fieldName: 'ip', + fieldValue: '30.156.16.163', + logRate: 'Chart type:bar chart', + pValue: '3.28e-13', + impact: 'High', + }, + { + fieldName: 'machine.os.keyword', + fieldValue: 'win xp', + logRate: 'Chart type:bar chart', + pValue: '0.0000997', + impact: 'Medium', + }, + { + fieldName: 'referer', + fieldValue: 'http://www.elastic-elastic-elastic.com/success/timothy-l-kopra', + logRate: 'Chart type:bar chart', + pValue: '4.74e-13', + impact: 'High', + }, + { + fieldName: 'response.keyword', + fieldValue: '404', + logRate: 'Chart type:bar chart', + pValue: '0.00000604', + impact: 'Medium', + }, + ], + fieldSelectorPopover: [ + 'clientip', + 'geo.dest', + 'geo.srcdest', + 'host.keyword', + 'ip', + 'machine.os.keyword', + 'referer', + 'response.keyword', + ], + }, +}; + export const farequoteDataViewTestData: TestData = { suiteTitle: 'farequote with spike', dataGenerator: 'farequote_with_spike', @@ -122,6 +227,7 @@ export const artificialLogDataViewTestData: TestData = { }; export const explainLogRateSpikesTestData: TestData[] = [ + kibanaLogsDataViewTestData, farequoteDataViewTestData, farequoteDataViewTestDataWithQuery, artificialLogDataViewTestData, diff --git a/x-pack/test/functional/apps/aiops/types.ts b/x-pack/test/functional/apps/aiops/types.ts index 01733a8e1a2af2..2093d4d9613634 100644 --- a/x-pack/test/functional/apps/aiops/types.ts +++ b/x-pack/test/functional/apps/aiops/types.ts @@ -7,6 +7,15 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +interface TestDataTableActionLogPatternAnalysis { + type: 'LogPatternAnalysis'; + tableRowId: string; + expected: { + queryBar: string; + totalDocCount: string; + }; +} + interface TestDataExpectedWithSampleProbability { totalDocCountFormatted: string; sampleProbabilityFormatted: string; @@ -40,11 +49,12 @@ export interface TestData { sourceIndexOrSavedSearch: string; rowsPerPage?: 10 | 25 | 50; brushBaselineTargetTimestamp?: number; - brushDeviationTargetTimestamp: number; + brushDeviationTargetTimestamp?: number; brushIntervalFactor: number; chartClickCoordinates: [number, number]; fieldSelectorSearch: string; fieldSelectorApplyAvailable: boolean; query?: string; + action?: TestDataTableActionLogPatternAnalysis; expected: TestDataExpectedWithSampleProbability | TestDataExpectedWithoutSampleProbability; } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts index 18cadebbf9afd9..b533c50677944d 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts @@ -5,12 +5,17 @@ * 2.0. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export function ExplainLogRateSpikesAnalysisGroupsTableProvider({ getService, }: FtrProviderContext) { + const find = getService('find'); const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const browser = getService('browser'); return new (class AnalysisTable { public async assertSpikeAnalysisTableExists() { @@ -55,5 +60,50 @@ export function ExplainLogRateSpikesAnalysisGroupsTableProvider({ return rows; } + + public rowSelector(rowId: string, subSelector?: string) { + const row = `~aiopsSpikeAnalysisGroupsTable > ~row-${rowId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async ensureActionsMenuOpen(rowId: string) { + await retry.tryForTime(30 * 1000, async () => { + await this.ensureActionsMenuClosed(); + + if (!(await find.existsByCssSelector('.euiContextMenuPanel', 1000))) { + await testSubjects.click(this.rowSelector(rowId, 'euiCollapsedItemActionsButton')); + expect(await find.existsByCssSelector('.euiContextMenuPanel', 1000)).to.eql( + true, + 'Actions popover should exist' + ); + } + }); + } + + public async ensureActionsMenuClosed() { + await retry.tryForTime(30 * 1000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + expect(await find.existsByCssSelector('.euiContextMenuPanel', 1000)).to.eql( + false, + 'Actions popover should not exist' + ); + }); + } + + public async assertRowActions(rowId: string) { + await this.ensureActionsMenuOpen(rowId); + + await testSubjects.existOrFail('aiopsTableActionButtonCopyToClipboard enabled'); + await testSubjects.existOrFail('aiopsTableActionButtonDiscover enabled'); + await testSubjects.existOrFail('aiopsTableActionButtonLogPatternAnalysis enabled'); + + await this.ensureActionsMenuClosed(); + } + + public async clickRowAction(rowId: string, action: string) { + await this.ensureActionsMenuOpen(rowId); + await testSubjects.click(`aiopsTableActionButton${action} enabled`); + await testSubjects.missingOrFail(`aiopsTableActionButton${action} enabled`); + } })(); } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts index 1a80ac679f29b8..228d47bbc746ff 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts @@ -122,6 +122,10 @@ export function ExplainLogRateSpikesDataGeneratorProvider({ getService }: FtrPro return new (class DataGenerator { public async generateData(dataGenerator: string) { switch (dataGenerator) { + case 'kibana_sample_data_logs': + // will be added via UI + break; + case 'farequote_with_spike': await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); @@ -191,6 +195,10 @@ export function ExplainLogRateSpikesDataGeneratorProvider({ getService }: FtrPro public async removeGeneratedData(dataGenerator: string) { switch (dataGenerator) { + case 'kibana_sample_data_logs': + // do not remove + break; + case 'farequote_with_spike': await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); break; diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 3da9ed7c760b76..736437a1d39767 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -229,9 +229,11 @@ export function ExplainLogRateSpikesPageProvider({ }, async assertProgressTitle(expectedProgressTitle: string) { - await testSubjects.existOrFail('aiopProgressTitle'); - const currentProgressTitle = await testSubjects.getVisibleText('aiopProgressTitle'); - expect(currentProgressTitle).to.be(expectedProgressTitle); + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopProgressTitle'); + const currentProgressTitle = await testSubjects.getVisibleText('aiopProgressTitle'); + expect(currentProgressTitle).to.be(expectedProgressTitle); + }); }, async navigateToIndexPatternSelection() { diff --git a/x-pack/test/functional/services/aiops/index.ts b/x-pack/test/functional/services/aiops/index.ts index 4816d37bcff049..8c208f182f3bd1 100644 --- a/x-pack/test/functional/services/aiops/index.ts +++ b/x-pack/test/functional/services/aiops/index.ts @@ -11,6 +11,7 @@ import { ExplainLogRateSpikesPageProvider } from './explain_log_rate_spikes_page import { ExplainLogRateSpikesAnalysisTableProvider } from './explain_log_rate_spikes_analysis_table'; import { ExplainLogRateSpikesAnalysisGroupsTableProvider } from './explain_log_rate_spikes_analysis_groups_table'; import { ExplainLogRateSpikesDataGeneratorProvider } from './explain_log_rate_spikes_data_generator'; +import { LogPatternAnalysisPageProvider } from './log_pattern_analysis_page'; export function AiopsProvider(context: FtrProviderContext) { const explainLogRateSpikesPage = ExplainLogRateSpikesPageProvider(context); @@ -18,11 +19,13 @@ export function AiopsProvider(context: FtrProviderContext) { const explainLogRateSpikesAnalysisGroupsTable = ExplainLogRateSpikesAnalysisGroupsTableProvider(context); const explainLogRateSpikesDataGenerator = ExplainLogRateSpikesDataGeneratorProvider(context); + const logPatternAnalysisPageProvider = LogPatternAnalysisPageProvider(context); return { explainLogRateSpikesPage, explainLogRateSpikesAnalysisTable, explainLogRateSpikesAnalysisGroupsTable, explainLogRateSpikesDataGenerator, + logPatternAnalysisPageProvider, }; } diff --git a/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts new file mode 100644 index 00000000000000..37872b8d7c0515 --- /dev/null +++ b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogPatternAnalysisPageProvider({ getService, getPageObject }: FtrProviderContext) { + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + return { + async assertLogCategorizationPageExists() { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopsLogCategorizationPage'); + }); + }, + + async assertQueryInput(expectedQueryString: string) { + const aiopsQueryInput = await testSubjects.find('aiopsQueryInput'); + const actualQueryString = await aiopsQueryInput.getVisibleText(); + expect(actualQueryString).to.eql( + expectedQueryString, + `Expected query bar text to be '${expectedQueryString}' (got '${actualQueryString}')` + ); + }, + + async assertTotalDocumentCount(expectedFormattedTotalDocCount: string) { + await retry.tryForTime(5000, async () => { + const docCount = await testSubjects.getVisibleText('aiopsTotalDocCount'); + expect(docCount).to.eql( + expectedFormattedTotalDocCount, + `Expected total document count to be '${expectedFormattedTotalDocCount}' (got '${docCount}')` + ); + }); + }, + }; +} From 06545277d7dfc379d69c2e68879fc409fa5b71fd Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 24 Apr 2023 15:02:44 -0300 Subject: [PATCH 57/65] [Infrastructure UI] Replace Snapshot API with InfraMetrics API in Hosts View (#155531) closes [#154443](https://github.com/elastic/kibana/issues/154443) ## Summary This PR replaces the usage of the Snapshot API in favor of the new `metrics/infra` endpoint and also includes a new control in the Search Bar to allow users to select how many hosts they want the API to return. https://user-images.githubusercontent.com/2767137/233728658-bccc7258-6955-47fb-8f7b-85ef6ec5d0f9.mov Because the KPIs now needs to show an "Average (of X hosts)", they will only start fetching the data once the table has been loaded. The hosts count KPI tile was not converted to Lens, because the page needs to know the total number of hosts. ### Possible follow-up Since now everything depends on the table to be loaded, I have been experimenting with batched requests to the new API. The idea is to fetch at least the host names as soon as possible. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../http_api/infra/get_infra_metrics.ts | 6 +- .../infra/public/hooks/use_lens_attributes.ts | 8 +- .../hosts/components/chart/lens_wrapper.tsx | 23 +++- .../components/chart/metric_chart_wrapper.tsx | 70 ++-------- .../hosts/components/hosts_container.tsx | 2 +- .../hosts/components/kpis/hosts_tile.tsx | 44 ++++-- .../hosts/components/kpis/kpi_grid.tsx | 62 +++------ .../metrics/hosts/components/kpis/tile.tsx | 64 +++++++-- .../{ => search_bar}/controls_content.tsx | 22 ++- .../components/search_bar/limit_options.tsx | 81 +++++++++++ .../search_bar/unified_search_bar.tsx | 126 +++++++++++++++++ .../components/tabs/logs/logs_tab_content.tsx | 7 +- .../components/tabs/metrics/metric_chart.tsx | 4 +- .../components/tabs/metrics/metrics_grid.tsx | 36 ++--- .../hosts/components/unified_search_bar.tsx | 98 ------------- .../public/pages/metrics/hosts/constants.ts | 6 +- .../metrics/hosts/hooks/use_alerts_query.ts | 4 +- .../metrics/hosts/hooks/use_data_view.ts | 1 + .../metrics/hosts/hooks/use_host_count.ts | 129 ++++++++++++++++++ .../hosts/hooks/use_hosts_table.test.ts | 31 +++-- .../metrics/hosts/hooks/use_hosts_table.tsx | 63 +++++---- .../metrics/hosts/hooks/use_hosts_view.ts | 91 ++++++------ .../metrics/hosts/hooks/use_unified_search.ts | 30 ++-- .../hooks/use_unified_search_url_state.ts | 18 ++- .../infra/public/pages/metrics/hosts/types.ts | 4 +- .../server/routes/infra/lib/constants.ts | 3 + x-pack/plugins/infra/tsconfig.json | 2 +- .../translations/translations/fr-FR.json | 10 +- .../translations/translations/ja-JP.json | 10 +- .../translations/translations/zh-CN.json | 10 +- .../api_integration/apis/metrics_ui/infra.ts | 4 + 31 files changed, 673 insertions(+), 396 deletions(-) rename x-pack/plugins/infra/public/pages/metrics/hosts/components/{ => search_bar}/controls_content.tsx (79%) create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx delete mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts diff --git a/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts b/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts index bcfeeafcee06fd..80e5e501169d63 100644 --- a/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts +++ b/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts @@ -23,8 +23,9 @@ export const RangeRT = rt.type({ }); export const InfraAssetMetadataTypeRT = rt.keyof({ - 'host.os.name': null, 'cloud.provider': null, + 'host.ip': null, + 'host.os.name': null, }); export const InfraAssetMetricsRT = rt.type({ @@ -35,7 +36,7 @@ export const InfraAssetMetricsRT = rt.type({ export const InfraAssetMetadataRT = rt.type({ // keep the actual field name from the index mappings name: InfraAssetMetadataTypeRT, - value: rt.union([rt.string, rt.number, rt.null]), + value: rt.union([rt.string, rt.null]), }); export const GetInfraMetricsRequestBodyPayloadRT = rt.intersection([ @@ -64,6 +65,7 @@ export const GetInfraMetricsResponsePayloadRT = rt.type({ export type InfraAssetMetrics = rt.TypeOf; export type InfraAssetMetadata = rt.TypeOf; +export type InfraAssetMetadataType = rt.TypeOf; export type InfraAssetMetricType = rt.TypeOf; export type InfraAssetMetricsItem = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts index c9ce48c909f9a4..6250d20750e296 100644 --- a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts @@ -74,11 +74,7 @@ export const useLensAttributes = ({ return visualizationAttributes; }, [dataView, formulaAPI, options, type, visualizationType]); - const injectFilters = (data: { - timeRange: TimeRange; - filters: Filter[]; - query: Query; - }): LensAttributes | null => { + const injectFilters = (data: { filters: Filter[]; query: Query }): LensAttributes | null => { if (!attributes) { return null; } @@ -121,7 +117,7 @@ export const useLensAttributes = ({ return true; }, async execute(_context: ActionExecutionContext): Promise { - const injectedAttributes = injectFilters({ timeRange, filters, query }); + const injectedAttributes = injectFilters({ filters, query }); if (injectedAttributes) { navigateToPrefilledEditor( { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx index 9a2472949f54cf..34e536aaf37d2b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx @@ -15,7 +15,7 @@ import { useIntersectedOnce } from '../../../../../hooks/use_intersection_once'; import { LensAttributes } from '../../../../../common/visualizations'; import { ChartLoader } from './chart_loader'; -export interface Props { +export interface LensWrapperProps { id: string; attributes: LensAttributes | null; dateRange: TimeRange; @@ -42,11 +42,12 @@ export const LensWrapper = ({ lastReloadRequestTime, loading = false, hasTitle = false, -}: Props) => { +}: LensWrapperProps) => { const intersectionRef = useRef(null); const [loadedOnce, setLoadedOnce] = useState(false); const [state, setState] = useState({ + attributes, lastReloadRequestTime, query, filters, @@ -65,15 +66,23 @@ export const LensWrapper = ({ useEffect(() => { if ((intersection?.intersectionRatio ?? 0) === 1) { setState({ + attributes, lastReloadRequestTime, query, - dateRange, filters, + dateRange, }); } - }, [dateRange, filters, intersection?.intersectionRatio, lastReloadRequestTime, query]); + }, [ + attributes, + dateRange, + filters, + intersection?.intersectionRatio, + lastReloadRequestTime, + query, + ]); - const isReady = attributes && intersectedOnce; + const isReady = state.attributes && intersectedOnce; return (
@@ -83,11 +92,11 @@ export const LensWrapper = ({ style={style} hasTitle={hasTitle} > - {isReady && ( + {state.attributes && ( ; - -type AcceptedType = SnapshotMetricType | 'hostsCount'; - -export interface ChartBaseProps - extends Pick< - MetricWTrend, - 'title' | 'color' | 'extra' | 'subtitle' | 'trendA11yDescription' | 'trendA11yTitle' - > { - type: AcceptedType; - toolTip: string; - metricType: MetricType; - ['data-test-subj']?: string; -} - -interface Props extends ChartBaseProps { +export interface Props extends Pick { id: string; - nodes: SnapshotNode[]; loading: boolean; - overrideValue?: number; + value: number; + toolTip: string; + ['data-test-subj']?: string; } const MIN_HEIGHT = 150; @@ -49,23 +25,13 @@ export const MetricChartWrapper = ({ extra, id, loading, - metricType, - nodes, - overrideValue, + value, subtitle, title, toolTip, - trendA11yDescription, - trendA11yTitle, - type, ...props }: Props) => { const loadedOnce = useRef(false); - const metrics = useMemo(() => (nodes ?? [])[0]?.metrics ?? [], [nodes]); - const metricsTimeseries = useMemo( - () => (metrics ?? []).find((m) => m.name === type)?.timeseries, - [metrics, type] - ); useEffect(() => { if (!loadedOnce.current && !loading) { @@ -76,29 +42,13 @@ export const MetricChartWrapper = ({ }; }, [loading]); - const metricsValue = useMemo(() => { - if (overrideValue) { - return overrideValue; - } - return (metrics ?? []).find((m) => m.name === type)?.[metricType] ?? 0; - }, [metricType, metrics, overrideValue, type]); - const metricsData: MetricWNumber = { title, subtitle, color, extra, - value: metricsValue, - valueFormatter: (d: number) => - type === 'hostsCount' ? d.toString() : createInventoryMetricFormatter({ type })(d), - ...(!!metricsTimeseries - ? { - trend: metricsTimeseries.rows.map((row) => ({ x: row.timestamp, y: row.metric_0 ?? 0 })), - trendShape: MetricTrendShape.Area, - trendA11yTitle, - trendA11yDescription, - } - : {}), + value, + valueFormatter: (d: number) => d.toString(), }; return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx index 0c965feca8e9ec..d42944857af344 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { InfraLoadingPanel } from '../../../../components/loading'; import { useMetricsDataViewContext } from '../hooks/use_data_view'; -import { UnifiedSearchBar } from './unified_search_bar'; +import { UnifiedSearchBar } from './search_bar/unified_search_bar'; import { HostsTable } from './hosts_table'; import { KPIGrid } from './kpis/kpi_grid'; import { Tabs } from './tabs/tabs'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx index 396c0bd72ad719..14a617682bf25a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx @@ -4,22 +4,46 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { i18n } from '@kbn/i18n'; import React from 'react'; +import { useHostCountContext } from '../../hooks/use_host_count'; +import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; -import { useHostsViewContext } from '../../hooks/use_hosts_view'; -import { type ChartBaseProps, MetricChartWrapper } from '../chart/metric_chart_wrapper'; +import { type Props, MetricChartWrapper } from '../chart/metric_chart_wrapper'; -export const HostsTile = ({ type, ...props }: ChartBaseProps) => { - const { hostNodes, loading } = useHostsViewContext(); +const HOSTS_CHART: Omit = { + id: `metric-hostCount`, + color: '#6DCCB1', + title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', { + defaultMessage: 'Hosts', + }), + toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', { + defaultMessage: 'The number of hosts returned by your current search criteria.', + }), + ['data-test-subj']: 'hostsView-metricsTrend-hosts', +}; + +export const HostsTile = () => { + const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext(); + const { searchCriteria } = useUnifiedSearchContext(); + + const getSubtitle = () => { + return searchCriteria.limit < (hostCountData?.count.value ?? 0) + ? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.hostCount.limit', { + defaultMessage: 'Limited to {limit}', + values: { + limit: searchCriteria.limit, + }, + }) + : undefined; + }; return ( ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx index 2dbd0c4324ecac..c3f751d26befbc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx @@ -9,10 +9,10 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { KPIChartProps, Tile } from './tile'; +import { HostCountProvider } from '../../hooks/use_host_count'; import { HostsTile } from './hosts_tile'; -import { ChartBaseProps } from '../chart/metric_chart_wrapper'; -const KPI_CHARTS: KPIChartProps[] = [ +const KPI_CHARTS: Array> = [ { type: 'cpu', trendLine: true, @@ -20,9 +20,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.title', { defaultMessage: 'CPU usage', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.tooltip', { defaultMessage: 'Average of percentage of CPU time spent in states other than Idle and IOWait, normalized by the number of CPU cores. Includes both time spent on user space and kernel space. 100% means all CPUs of the host are busy.', @@ -35,9 +32,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.title', { defaultMessage: 'Memory usage', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.tooltip', { defaultMessage: "Average of percentage of main memory usage excluding page cache. This includes resident memory for all processes plus memory used by the kernel structures and code apart the page cache. A high level indicates a situation of memory saturation for a host. 100% means the main memory is entirely filled with memory that can't be reclaimed, except by swapping out.", @@ -50,9 +44,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.title', { defaultMessage: 'Network inbound (RX)', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.tooltip', { defaultMessage: 'Number of bytes which have been received per second on the public interfaces of the hosts.', @@ -65,9 +56,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.title', { defaultMessage: 'Network outbound (TX)', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.tooltip', { defaultMessage: 'Number of bytes which have been received per second on the public interfaces of the hosts.', @@ -75,38 +63,24 @@ const KPI_CHARTS: KPIChartProps[] = [ }, ]; -const HOSTS_CHART: ChartBaseProps = { - type: 'hostsCount', - color: '#6DCCB1', - metricType: 'value', - title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', { - defaultMessage: 'Hosts', - }), - trendA11yTitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.a11y.title', { - defaultMessage: 'Hosts count.', - }), - toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', { - defaultMessage: 'The number of hosts returned by your current search criteria.', - }), - ['data-test-subj']: 'hostsView-metricsTrend-hosts', -}; - export const KPIGrid = () => { return ( - - - - - {KPI_CHARTS.map(({ ...chartProp }) => ( - - + + + + - ))} - + {KPI_CHARTS.map(({ ...chartProp }) => ( + + + + ))} + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx index a95f18b4a10ee8..89eebeefd240e9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; -import { Action } from '@kbn/ui-actions-plugin/public'; +import { i18n } from '@kbn/i18n'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; import { EuiIcon, @@ -24,6 +24,9 @@ import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; import { HostsLensMetricChartFormulas } from '../../../../../common/visualizations'; import { useHostsViewContext } from '../../hooks/use_hosts_view'; import { LensWrapper } from '../chart/lens_wrapper'; +import { createHostsFilter } from '../../utils'; +import { useHostCountContext } from '../../hooks/use_host_count'; +import { useAfterLoadedState } from '../../hooks/use_after_loaded_state'; export interface KPIChartProps { title: string; @@ -38,7 +41,6 @@ const MIN_HEIGHT = 150; export const Tile = ({ title, - subtitle, type, backgroundColor, toolTip, @@ -46,14 +48,28 @@ export const Tile = ({ }: KPIChartProps) => { const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest } = useHostsViewContext(); + const { requestTs, hostNodes, loading: hostsLoading } = useHostsViewContext(); + const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext(); + + const getSubtitle = () => { + return searchCriteria.limit < (hostCountData?.count.value ?? 0) + ? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.average.limit', { + defaultMessage: 'Average (of {limit} hosts)', + values: { + limit: searchCriteria.limit, + }, + }) + : i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.average', { + defaultMessage: 'Average', + }); + }; const { attributes, getExtraActions, error } = useLensAttributes({ type, dataView, options: { title, - subtitle, + subtitle: getSubtitle(), backgroundColor, showTrendLine: trendLine, showTitle: false, @@ -61,15 +77,24 @@ export const Tile = ({ visualizationType: 'metricChart', }); - const filters = [...searchCriteria.filters, ...searchCriteria.panelFilters]; + const hostsFilterQuery = useMemo(() => { + return createHostsFilter( + hostNodes.map((p) => p.name), + dataView + ); + }, [hostNodes, dataView]); + + const filters = useMemo( + () => [...searchCriteria.filters, ...searchCriteria.panelFilters, ...[hostsFilterQuery]], + [hostsFilterQuery, searchCriteria.filters, searchCriteria.panelFilters] + ); + const extraActionOptions = getExtraActions({ timeRange: searchCriteria.dateRange, filters, query: searchCriteria.query, }); - const extraActions: Action[] = [extraActionOptions.openInLens]; - const handleBrushEnd = ({ range }: BrushTriggerEvent['data']) => { const [min, max] = range; onSubmit({ @@ -81,6 +106,14 @@ export const Tile = ({ }); }; + const loading = hostsLoading || !attributes || hostCountLoading; + const { afterLoadedState } = useAfterLoadedState(loading, { + attributes, + lastReloadRequestTime: requestTs, + ...searchCriteria, + filters, + }); + return ( )} @@ -134,7 +168,7 @@ export const Tile = ({ const EuiPanelStyled = styled(EuiPanel)` .echMetric { - border-radius: ${(p) => p.theme.eui.euiBorderRadius}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; pointer-events: none; } `; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx similarity index 79% rename from x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx rename to x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx index a3e82b99014227..e2bd7d0c74daef 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx @@ -12,15 +12,16 @@ import { type ControlGroupInput, } from '@kbn/controls-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; -import type { Filter, Query, TimeRange } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, Filter, Query, TimeRange } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/public'; -import { Subscription } from 'rxjs'; -import { useControlPanels } from '../hooks/use_control_panels_url_state'; +import { skipWhile, Subscription } from 'rxjs'; +import { useControlPanels } from '../../hooks/use_control_panels_url_state'; interface Props { dataView: DataView | undefined; timeRange: TimeRange; filters: Filter[]; + selectedOptions: Filter[]; query: Query; onFiltersChange: (filters: Filter[]) => void; } @@ -29,6 +30,7 @@ export const ControlsContent: React.FC = ({ dataView, filters, query, + selectedOptions, timeRange, onFiltersChange, }) => { @@ -55,15 +57,21 @@ export const ControlsContent: React.FC = ({ const loadCompleteHandler = useCallback( (controlGroup: ControlGroupAPI) => { if (!controlGroup) return; - inputSubscription.current = controlGroup.onFiltersPublished$.subscribe((newFilters) => { - onFiltersChange(newFilters); - }); + inputSubscription.current = controlGroup.onFiltersPublished$ + .pipe( + skipWhile((newFilters) => + compareFilters(selectedOptions, newFilters, COMPARE_ALL_OPTIONS) + ) + ) + .subscribe((newFilters) => { + onFiltersChange(newFilters); + }); filterSubscription.current = controlGroup .getInput$() .subscribe(({ panels }) => setControlPanels(panels)); }, - [onFiltersChange, setControlPanels] + [onFiltersChange, setControlPanels, selectedOptions] ); useEffect(() => { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx new file mode 100644 index 00000000000000..1d8ce4f9c9e87a --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiButtonGroup, + EuiButtonGroupOptionProps, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiToolTip, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { HOST_LIMIT_OPTIONS } from '../../constants'; +import { HostLimitOptions } from '../../types'; + +interface Props { + limit: HostLimitOptions; + onChange: (limit: number) => void; +} + +export const LimitOptions = ({ limit, onChange }: Props) => { + return ( + + + + + + + + + + + + + + + onChange(value)} + /> + + + ); +}; + +const buildId = (option: number) => `hostLimit_${option}`; +const options: EuiButtonGroupOptionProps[] = HOST_LIMIT_OPTIONS.map((option) => ({ + id: buildId(option), + label: `${option}`, + value: option, + 'data-test-subj': `hostsViewLimitSelection${option}button`, +})); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx new file mode 100644 index 00000000000000..ef515cc018839b --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGrid, + useEuiTheme, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { METRICS_APP_DATA_TEST_SUBJ } from '../../../../../apps/metrics_app'; +import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; +import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; +import { ControlsContent } from './controls_content'; +import { useMetricsDataViewContext } from '../../hooks/use_data_view'; +import { HostsSearchPayload } from '../../hooks/use_unified_search_url_state'; +import { LimitOptions } from './limit_options'; +import { HostLimitOptions } from '../../types'; + +export const UnifiedSearchBar = () => { + const { + services: { unifiedSearch, application }, + } = useKibanaContextForPlugin(); + const { dataView } = useMetricsDataViewContext(); + const { searchCriteria, onSubmit } = useUnifiedSearchContext(); + + const { SearchBar } = unifiedSearch.ui; + + const onLimitChange = (limit: number) => { + onSubmit({ limit }); + }; + + const onPanelFiltersChange = (panelFilters: Filter[]) => { + if (!compareFilters(searchCriteria.panelFilters, panelFilters, COMPARE_ALL_OPTIONS)) { + onSubmit({ panelFilters }); + } + }; + + const handleRefresh = (payload: HostsSearchPayload, isUpdate?: boolean) => { + // This makes sure `onQueryChange` is only called when the submit button is clicked + if (isUpdate === false) { + onSubmit(payload); + } + }; + + return ( + + + + 0.5)', + })} + onQuerySubmit={handleRefresh} + showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)} + showDatePicker + showFilterBar + showQueryInput + showQueryMenu + useDefaultBehaviors + /> + + + + + + + + + + + + + + + ); +}; + +const StickyContainer = (props: { children: React.ReactNode }) => { + const { euiTheme } = useEuiTheme(); + + const top = useMemo(() => { + const wrapper = document.querySelector(`[data-test-subj="${METRICS_APP_DATA_TEST_SUBJ}"]`); + if (!wrapper) { + return `calc(${euiTheme.size.xxxl} * 2)`; + } + + return `${wrapper.getBoundingClientRect().top}px`; + }, [euiTheme]); + + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index d5cc0b0f021d76..6813dee1caa105 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -9,7 +9,6 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { InfraLoadingPanel } from '../../../../../../components/loading'; -import { SnapshotNode } from '../../../../../../../common/http_api'; import { LogStream } from '../../../../../../components/log_stream'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; @@ -30,7 +29,7 @@ export const LogsTabContent = () => { ); const logsLinkToStreamQuery = useMemo(() => { - const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes); + const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes.map((p) => p.name)); if (filterQuery.query && hostsFilterQueryParam) { return `${filterQuery.query} and ${hostsFilterQueryParam}`; @@ -83,12 +82,12 @@ export const LogsTabContent = () => { ); }; -const createHostsFilterQueryParam = (hostNodes: SnapshotNode[]): string => { +const createHostsFilterQueryParam = (hostNodes: string[]): string => { if (!hostNodes.length) { return ''; } - const joinedHosts = hostNodes.map((p) => p.name).join(' or '); + const joinedHosts = hostNodes.join(' or '); const hostsQueryParam = `host.name:(${joinedHosts})`; return hostsQueryParam; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx index 28d07b94d94377..f81228957107ae 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx @@ -40,13 +40,13 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => const { euiTheme } = useEuiTheme(); const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest, loading } = useHostsViewContext(); + const { requestTs, loading } = useHostsViewContext(); const { currentPage } = useHostsTableContext(); // prevents updates on requestTs and serchCriteria states from relaoding the chart // we want it to reload only once the table has finished loading const { afterLoadedState } = useAfterLoadedState(loading, { - lastReloadRequestTime: baseRequest.requestTs, + lastReloadRequestTime: requestTs, ...searchCriteria, }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx index e307dde0d09e52..7f3dac7a3af163 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexGrid, EuiFlexItem, EuiFlexGroup, EuiText, EuiI18n } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { MetricChart, MetricChartProps } from './metric_chart'; @@ -64,32 +64,12 @@ const CHARTS_IN_ORDER: Array & { fullRo export const MetricsGrid = React.memo(() => { return ( - - - - - - {DEFAULT_BREAKDOWN_SIZE}, - attribute: name, - }} - /> - - - - - - - {CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => ( - - - - ))} - - - + + {CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => ( + + + + ))} + ); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx deleted file mode 100644 index 168f825a9d2d18..00000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx +++ /dev/null @@ -1,98 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo } from 'react'; -import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGrid, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { EuiHorizontalRule } from '@elastic/eui'; -import { METRICS_APP_DATA_TEST_SUBJ } from '../../../../apps/metrics_app'; -import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { useUnifiedSearchContext } from '../hooks/use_unified_search'; -import { ControlsContent } from './controls_content'; -import { useMetricsDataViewContext } from '../hooks/use_data_view'; -import { HostsSearchPayload } from '../hooks/use_unified_search_url_state'; - -export const UnifiedSearchBar = () => { - const { - services: { unifiedSearch, application }, - } = useKibanaContextForPlugin(); - const { dataView } = useMetricsDataViewContext(); - const { searchCriteria, onSubmit } = useUnifiedSearchContext(); - - const { SearchBar } = unifiedSearch.ui; - - const onPanelFiltersChange = (panelFilters: Filter[]) => { - if (!compareFilters(searchCriteria.panelFilters, panelFilters, COMPARE_ALL_OPTIONS)) { - onSubmit({ panelFilters }); - } - }; - - const handleRefresh = (payload: HostsSearchPayload, isUpdate?: boolean) => { - // This makes sure `onQueryChange` is only called when the submit button is clicked - if (isUpdate === false) { - onSubmit(payload); - } - }; - - return ( - - 0.5)', - })} - onQuerySubmit={handleRefresh} - showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)} - showDatePicker - showFilterBar - showQueryInput - showQueryMenu - useDefaultBehaviors - /> - - - - ); -}; - -const StickyContainer = (props: { children: React.ReactNode }) => { - const { euiTheme } = useEuiTheme(); - - const top = useMemo(() => { - const wrapper = document.querySelector(`[data-test-subj="${METRICS_APP_DATA_TEST_SUBJ}"]`); - if (!wrapper) { - return `calc(${euiTheme.size.xxxl} * 2)`; - } - - return `${wrapper.getBoundingClientRect().top}px`; - }, [euiTheme]); - - return ( - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index b854120a868879..69cfc446d00950 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -7,13 +7,15 @@ import { i18n } from '@kbn/i18n'; import { ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; -import { AlertStatusFilter } from './types'; +import { AlertStatusFilter, HostLimitOptions } from './types'; export const ALERT_STATUS_ALL = 'all'; export const TIMESTAMP_FIELD = '@timestamp'; export const DATA_VIEW_PREFIX = 'infra_metrics'; +export const DEFAULT_HOST_LIMIT: HostLimitOptions = 100; export const DEFAULT_PAGE_SIZE = 10; +export const LOCAL_STORAGE_HOST_LIMIT_KEY = 'hostsView:hostLimitSelection'; export const LOCAL_STORAGE_PAGE_SIZE_KEY = 'hostsView:pageSizeSelection'; export const ALL_ALERTS: AlertStatusFilter = { @@ -55,3 +57,5 @@ export const ALERT_STATUS_QUERY = { [ACTIVE_ALERTS.status]: ACTIVE_ALERTS.query, [RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query, }; + +export const HOST_LIMIT_OPTIONS = [10, 20, 50, 100, 500] as const; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts index 7a895591d68c7f..200bff521d86a7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts @@ -9,7 +9,7 @@ import createContainer from 'constate'; import { getTime } from '@kbn/data-plugin/common'; import { ALERT_TIME_RANGE } from '@kbn/rule-data-utils'; import { BoolQuery, buildEsQuery, Filter } from '@kbn/es-query'; -import { SnapshotNode } from '../../../../../common/http_api'; +import { InfraAssetMetricsItem } from '../../../../../common/http_api'; import { useUnifiedSearchContext } from './use_unified_search'; import { HostsState } from './use_unified_search_url_state'; import { useHostsViewContext } from './use_hosts_view'; @@ -63,7 +63,7 @@ const createAlertsEsQuery = ({ status, }: { dateRange: HostsState['dateRange']; - hostNodes: SnapshotNode[]; + hostNodes: InfraAssetMetricsItem[]; status?: AlertStatus; }): AlertsEsQuery => { const alertStatusFilter = createAlertStatusFilter(status); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts index 94e3a963075bea..83fedf42929370 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts @@ -72,6 +72,7 @@ export const useDataView = ({ metricAlias }: { metricAlias: string }) => { }, [hasError, notifications, metricAlias]); return { + metricAlias, dataView, loading, hasError, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts new file mode 100644 index 00000000000000..5575c46e621f15 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { ES_SEARCH_STRATEGY, IKibanaSearchResponse } from '@kbn/data-plugin/common'; +import { useCallback, useEffect } from 'react'; +import { catchError, map, Observable, of, startWith } from 'rxjs'; +import createContainer from 'constate'; +import type { QueryDslQueryContainer, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { useDataSearch, useLatestPartialDataSearchResponse } from '../../../../utils/data_search'; +import { useMetricsDataViewContext } from './use_data_view'; +import { useUnifiedSearchContext } from './use_unified_search'; + +export const useHostCount = () => { + const { dataView, metricAlias } = useMetricsDataViewContext(); + const { buildQuery, getParsedDateRange } = useUnifiedSearchContext(); + + const { search: fetchHostCount, requests$ } = useDataSearch({ + getRequest: useCallback(() => { + const query = buildQuery(); + const dateRange = getParsedDateRange(); + + const filters: QueryDslQueryContainer = { + bool: { + ...query.bool, + filter: [ + ...query.bool.filter, + { + exists: { + field: 'host.name', + }, + }, + { + range: { + [dataView?.timeFieldName ?? '@timestamp']: { + gte: dateRange.from, + lte: dateRange.to, + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }; + + return { + request: { + params: { + allow_no_indices: true, + ignore_unavailable: true, + index: metricAlias, + size: 0, + track_total_hits: false, + body: { + query: filters, + aggs: { + count: { + cardinality: { + field: 'host.name', + }, + }, + }, + }, + }, + }, + options: { strategy: ES_SEARCH_STRATEGY }, + }; + }, [buildQuery, dataView, getParsedDateRange, metricAlias]), + parseResponses: normalizeDataSearchResponse, + }); + + const { isRequestRunning, isResponsePartial, latestResponseData, latestResponseErrors } = + useLatestPartialDataSearchResponse(requests$); + + useEffect(() => { + fetchHostCount(); + }, [fetchHostCount]); + + return { + errors: latestResponseErrors, + isRequestRunning, + isResponsePartial, + data: latestResponseData ?? null, + }; +}; + +export const HostCount = createContainer(useHostCount); +export const [HostCountProvider, useHostCountContext] = HostCount; + +const INITIAL_STATE = { + data: null, + errors: [], + isPartial: true, + isRunning: true, + loaded: 0, + total: undefined, +}; +const normalizeDataSearchResponse = ( + response$: Observable>>> +) => + response$.pipe( + map((response) => ({ + data: decodeOrThrow(HostCountResponseRT)(response.rawResponse.aggregations), + errors: [], + isPartial: response.isPartial ?? false, + isRunning: response.isRunning ?? false, + loaded: response.loaded, + total: response.total, + })), + startWith(INITIAL_STATE), + catchError((error) => + of({ + ...INITIAL_STATE, + errors: [error.message ?? error], + isRunning: false, + }) + ) + ); + +const HostCountResponseRT = rt.type({ + count: rt.type({ + value: rt.number, + }), +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts index a921a0daeb011f..5619a788b19a7b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts @@ -7,7 +7,7 @@ import { useHostsTable } from './use_hosts_table'; import { renderHook } from '@testing-library/react-hooks'; -import { SnapshotNode } from '../../../../../common/http_api'; +import { InfraAssetMetricsItem } from '../../../../../common/http_api'; import * as useUnifiedSearchHooks from './use_unified_search'; import * as useHostsViewHooks from './use_hosts_view'; @@ -22,20 +22,20 @@ const mockUseHostsViewContext = useHostsViewHooks.useHostsViewContext as jest.Mo typeof useHostsViewHooks.useHostsViewContext >; -const mockHostNode: SnapshotNode[] = [ +const mockHostNode: InfraAssetMetricsItem[] = [ { metrics: [ { name: 'rx', - avg: 252456.92916666667, + value: 252456.92916666667, }, { name: 'tx', - avg: 252758.425, + value: 252758.425, }, { name: 'memory', - avg: 0.94525, + value: 0.94525, }, { name: 'cpu', @@ -43,25 +43,28 @@ const mockHostNode: SnapshotNode[] = [ }, { name: 'memoryTotal', - avg: 34359.738368, + value: 34359.738368, }, ], - path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], + metadata: [ + { name: 'host.os.name', value: null }, + { name: 'cloud.provider', value: 'aws' }, + ], name: 'host-0', }, { metrics: [ { name: 'rx', - avg: 95.86339715321859, + value: 95.86339715321859, }, { name: 'tx', - avg: 110.38566859563191, + value: 110.38566859563191, }, { name: 'memory', - avg: 0.5400000214576721, + value: 0.5400000214576721, }, { name: 'cpu', @@ -69,12 +72,12 @@ const mockHostNode: SnapshotNode[] = [ }, { name: 'memoryTotal', - avg: 9.194304, + value: 9.194304, }, ], - path: [ - { value: 'host-1', label: 'host-1' }, - { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, + metadata: [ + { name: 'host.os.name', value: 'macOS' }, + { name: 'host.ip', value: '243.86.94.22' }, ], name: 'host-1', }, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 2d2d6c9d7f8e44..7350f402c57ec0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -15,10 +15,10 @@ import { isNumber } from 'lodash/fp'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter'; import { HostsTableEntryTitle } from '../components/hosts_table_entry_title'; -import type { - SnapshotNode, - SnapshotNodeMetric, - SnapshotMetricInput, +import { + InfraAssetMetadataType, + InfraAssetMetricsItem, + InfraAssetMetricType, } from '../../../../../common/http_api'; import { useHostFlyoutOpen } from './use_host_flyout_open_url_state'; import { Sorting, useHostsTableProperties } from './use_hosts_table_url_state'; @@ -29,42 +29,55 @@ import { useUnifiedSearchContext } from './use_unified_search'; * Columns and items types */ export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider'; +type HostMetrics = Record; -type HostMetric = 'cpu' | 'diskLatency' | 'rx' | 'tx' | 'memory' | 'memoryTotal'; - -type HostMetrics = Record; - -export interface HostNodeRow extends HostMetrics { +interface HostMetadata { os?: string | null; ip?: string | null; servicesOnHost?: number | null; title: { name: string; cloudProvider?: CloudProvider | null }; - name: string; id: string; } +export type HostNodeRow = HostMetadata & + HostMetrics & { + name: string; + }; /** * Helper functions */ -const formatMetric = (type: SnapshotMetricInput['type'], value: number | undefined | null) => { +const formatMetric = (type: InfraAssetMetricType, value: number | undefined | null) => { return value || value === 0 ? createInventoryMetricFormatter({ type })(value) : 'N/A'; }; -const buildItemsList = (nodes: SnapshotNode[]) => { - return nodes.map(({ metrics, path, name }) => ({ - id: `${name}-${path.at(-1)?.os ?? '-'}`, - name, - os: path.at(-1)?.os ?? '-', - ip: path.at(-1)?.ip ?? '', - title: { +const buildItemsList = (nodes: InfraAssetMetricsItem[]): HostNodeRow[] => { + return nodes.map(({ metrics, metadata, name }) => { + const metadataKeyValue = metadata.reduce( + (acc, curr) => ({ + ...acc, + [curr.name]: curr.value, + }), + {} as Record + ); + + return { name, - cloudProvider: path.at(-1)?.cloudProvider ?? null, - }, - ...metrics.reduce((data, metric) => { - data[metric.name as HostMetric] = metric.avg ?? metric.value; - return data; - }, {} as HostMetrics), - })) as HostNodeRow[]; + id: `${name}-${metadataKeyValue['host.os.name'] ?? '-'}`, + title: { + name, + cloudProvider: (metadataKeyValue['cloud.provider'] as CloudProvider) ?? null, + }, + os: metadataKeyValue['host.os.name'] ?? '-', + ip: metadataKeyValue['host.ip'] ?? '', + ...metrics.reduce( + (acc, curr) => ({ + ...acc, + [curr.name]: curr.value ?? 0, + }), + {} as HostMetrics + ), + }; + }); }; const isTitleColumn = (cell: any): cell is HostNodeRow['title'] => { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts index d0df961dc7ef9c..f84acf5931ea1a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts @@ -12,16 +12,21 @@ * 2.0. */ -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import createContainer from 'constate'; import { BoolQuery } from '@kbn/es-query'; -import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { useSourceContext } from '../../../../containers/metrics_source'; -import { useSnapshot, type UseSnapshotRequest } from '../../inventory_view/hooks/use_snaphot'; import { useUnifiedSearchContext } from './use_unified_search'; -import { StringDateRangeTimestamp } from './use_unified_search_url_state'; +import { + GetInfraMetricsRequestBodyPayload, + GetInfraMetricsResponsePayload, + InfraAssetMetricType, +} from '../../../../../common/http_api'; +import { StringDateRange } from './use_unified_search_url_state'; -const HOST_TABLE_METRICS: Array<{ type: SnapshotMetricType }> = [ +const HOST_TABLE_METRICS: Array<{ type: InfraAssetMetricType }> = [ { type: 'rx' }, { type: 'tx' }, { type: 'memory' }, @@ -30,40 +35,52 @@ const HOST_TABLE_METRICS: Array<{ type: SnapshotMetricType }> = [ { type: 'memoryTotal' }, ]; +const BASE_INFRA_METRICS_PATH = '/api/metrics/infra'; + export const useHostsView = () => { const { sourceId } = useSourceContext(); - const { buildQuery, getDateRangeAsTimestamp } = useUnifiedSearchContext(); + const { + services: { http }, + } = useKibanaContextForPlugin(); + const { buildQuery, getParsedDateRange, searchCriteria } = useUnifiedSearchContext(); + const abortCtrlRef = useRef(new AbortController()); const baseRequest = useMemo( () => - createSnapshotRequest({ - dateRange: getDateRangeAsTimestamp(), + createInfraMetricsRequest({ + dateRange: getParsedDateRange(), esQuery: buildQuery(), sourceId, + limit: searchCriteria.limit, }), - [buildQuery, getDateRangeAsTimestamp, sourceId] + [buildQuery, getParsedDateRange, sourceId, searchCriteria.limit] ); - // Snapshot endpoint internally uses the indices stored in source.configuration.metricAlias. - // For the Unified Search, we create a data view, which for now will be built off of source.configuration.metricAlias too - // if we introduce data view selection, we'll have to change this hook and the endpoint to accept a new parameter for the indices - const { - loading, - error, - nodes: hostNodes, - } = useSnapshot( - { - ...baseRequest, - metrics: HOST_TABLE_METRICS, + const [state, refetch] = useAsyncFn( + () => { + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + return http.post(`${BASE_INFRA_METRICS_PATH}`, { + signal: abortCtrlRef.current.signal, + body: JSON.stringify(baseRequest), + }); }, - { abortable: true } + [baseRequest, http], + { loading: true } ); + useEffect(() => { + refetch(); + }, [refetch]); + + const { value, error, loading } = state; + return { - baseRequest, + requestTs: baseRequest.requestTs, loading, error, - hostNodes, + hostNodes: value?.nodes ?? [], }; }; @@ -73,30 +90,26 @@ export const [HostsViewProvider, useHostsViewContext] = HostsView; /** * Helpers */ -const createSnapshotRequest = ({ + +const createInfraMetricsRequest = ({ esQuery, sourceId, dateRange, + limit, }: { esQuery: { bool: BoolQuery }; sourceId: string; - dateRange: StringDateRangeTimestamp; -}): UseSnapshotRequest => ({ - filterQuery: JSON.stringify(esQuery), - metrics: [], - groupBy: [], - nodeType: 'host', - sourceId, - currentTime: dateRange.to, - includeTimeseries: false, - sendRequestImmediately: true, - timerange: { - interval: '1m', + dateRange: StringDateRange; + limit: number; +}): GetInfraMetricsRequestBodyPayload & { requestTs: number } => ({ + type: 'host', + query: esQuery, + range: { from: dateRange.from, to: dateRange.to, - ignoreLookback: true, }, - // The user might want to click on the submit button without changing the filters - // This makes sure all child components will re-render. + metrics: HOST_TABLE_METRICS, + limit, + sourceId, requestTs: Date.now(), }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts index 950bd9cf3c94e7..e242f58054c6ca 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts @@ -23,14 +23,14 @@ import { } from './use_unified_search_url_state'; const buildQuerySubmittedPayload = ( - hostState: HostsState & { dateRangeTimestamp: StringDateRangeTimestamp } + hostState: HostsState & { parsedDateRange: StringDateRangeTimestamp } ) => { - const { panelFilters, filters, dateRangeTimestamp, query: queryObj } = hostState; + const { panelFilters, filters, parsedDateRange, query: queryObj } = hostState; return { control_filters: panelFilters.map((filter) => JSON.stringify(filter)), filters: filters.map((filter) => JSON.stringify(filter)), - interval: telemetryTimeRangeFormatter(dateRangeTimestamp.to - dateRangeTimestamp.from), + interval: telemetryTimeRangeFormatter(parsedDateRange.to - parsedDateRange.from), query: queryObj.query, }; }; @@ -41,8 +41,8 @@ const getDefaultTimestamps = () => { const now = Date.now(); return { - from: now - DEFAULT_FROM_IN_MILLISECONDS, - to: now, + from: new Date(now - DEFAULT_FROM_IN_MILLISECONDS).toISOString(), + to: new Date(now).toISOString(), }; }; @@ -63,16 +63,25 @@ export const useUnifiedSearch = () => { const onSubmit = (params?: HostsSearchPayload) => setSearch(params ?? {}); - const getDateRangeAsTimestamp = useCallback(() => { + const getParsedDateRange = useCallback(() => { const defaults = getDefaultTimestamps(); - const from = DateMath.parse(searchCriteria.dateRange.from)?.valueOf() ?? defaults.from; + const from = DateMath.parse(searchCriteria.dateRange.from)?.toISOString() ?? defaults.from; const to = - DateMath.parse(searchCriteria.dateRange.to, { roundUp: true })?.valueOf() ?? defaults.to; + DateMath.parse(searchCriteria.dateRange.to, { roundUp: true })?.toISOString() ?? defaults.to; return { from, to }; }, [searchCriteria.dateRange]); + const getDateRangeAsTimestamp = useCallback(() => { + const parsedDate = getParsedDateRange(); + + const from = new Date(parsedDate.from).getTime(); + const to = new Date(parsedDate.to).getTime(); + + return { from, to }; + }, [getParsedDateRange]); + const buildQuery = useCallback(() => { return buildEsQuery(dataView, searchCriteria.query, [ ...searchCriteria.filters, @@ -116,15 +125,16 @@ export const useUnifiedSearch = () => { // Track telemetry event on query/filter/date changes useEffect(() => { - const dateRangeTimestamp = getDateRangeAsTimestamp(); + const parsedDateRange = getDateRangeAsTimestamp(); telemetry.reportHostsViewQuerySubmitted( - buildQuerySubmittedPayload({ ...searchCriteria, dateRangeTimestamp }) + buildQuerySubmittedPayload({ ...searchCriteria, parsedDateRange }) ); }, [getDateRangeAsTimestamp, searchCriteria, telemetry]); return { buildQuery, onSubmit, + getParsedDateRange, getDateRangeAsTimestamp, searchCriteria, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts index 861f3c26472e81..bae9f2ed3f713f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts @@ -13,11 +13,13 @@ import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import { enumeration } from '@kbn/securitysolution-io-ts-types'; import { FilterStateStore } from '@kbn/es-query'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { useUrlState } from '../../../../utils/use_url_state'; import { useKibanaTimefilterTime, useSyncKibanaTimeFilterTime, } from '../../../../hooks/use_kibana_timefilter_time'; +import { DEFAULT_HOST_LIMIT, LOCAL_STORAGE_HOST_LIMIT_KEY } from '../constants'; const DEFAULT_QUERY = { language: 'kuery', @@ -32,6 +34,7 @@ const INITIAL_HOSTS_STATE: HostsState = { filters: [], panelFilters: [], dateRange: INITIAL_DATE_RANGE, + limit: DEFAULT_HOST_LIMIT, }; const reducer = (prevState: HostsState, params: HostsSearchPayload) => { @@ -45,9 +48,17 @@ const reducer = (prevState: HostsState, params: HostsSearchPayload) => { export const useHostsUrlState = (): [HostsState, HostsStateUpdater] => { const [getTime] = useKibanaTimefilterTime(INITIAL_DATE_RANGE); + const [localStorageHostLimit, setLocalStorageHostLimit] = useLocalStorage( + LOCAL_STORAGE_HOST_LIMIT_KEY, + INITIAL_HOSTS_STATE.limit + ); const [urlState, setUrlState] = useUrlState({ - defaultState: { ...INITIAL_HOSTS_STATE, dateRange: getTime() }, + defaultState: { + ...INITIAL_HOSTS_STATE, + dateRange: getTime(), + limit: localStorageHostLimit ?? INITIAL_HOSTS_STATE.limit, + }, decodeUrlState, encodeUrlState, urlStateKey: '_a', @@ -57,6 +68,9 @@ export const useHostsUrlState = (): [HostsState, HostsStateUpdater] => { const [search, setSearch] = useReducer(reducer, urlState); if (!deepEqual(search, urlState)) { setUrlState(search); + if (localStorageHostLimit !== search.limit) { + setLocalStorageHostLimit(search.limit); + } } useSyncKibanaTimeFilterTime(INITIAL_DATE_RANGE, urlState.dateRange, (dateRange) => @@ -110,6 +124,7 @@ const HostsStateRT = rt.type({ panelFilters: HostsFiltersRT, query: HostsQueryStateRT, dateRange: StringDateRangeRT, + limit: rt.number, }); export type HostsState = rt.TypeOf; @@ -118,6 +133,7 @@ export type HostsSearchPayload = Partial; export type HostsStateUpdater = (params: HostsSearchPayload) => void; +export type StringDateRange = rt.TypeOf; export interface StringDateRangeTimestamp { from: number; to: number; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts index 6b948fb0da6c98..080b47f54d4da3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts @@ -7,7 +7,7 @@ import { Filter } from '@kbn/es-query'; import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; -import { ALERT_STATUS_ALL } from './constants'; +import { ALERT_STATUS_ALL, HOST_LIMIT_OPTIONS } from './constants'; export type AlertStatus = | typeof ALERT_STATUS_ACTIVE @@ -19,3 +19,5 @@ export interface AlertStatusFilter { query?: Filter['query']; label: string; } + +export type HostLimitOptions = typeof HOST_LIMIT_OPTIONS[number]; diff --git a/x-pack/plugins/infra/server/routes/infra/lib/constants.ts b/x-pack/plugins/infra/server/routes/infra/lib/constants.ts index bc9131d1d52fc3..f39eaafdd039e0 100644 --- a/x-pack/plugins/infra/server/routes/infra/lib/constants.ts +++ b/x-pack/plugins/infra/server/routes/infra/lib/constants.ts @@ -24,6 +24,9 @@ export const METADATA_AGGREGATION: Record Date: Mon, 24 Apr 2023 20:15:18 +0200 Subject: [PATCH 58/65] Fleet: allow Universal Profiling symbolizer permissions on indices (#155642) ## Summary For the introduction of the Universal Profiling symbolizer in Cloud, Fleet needs an update. The reason for Universal Profiling symbolizer to be different from other packages running via Fleet is that: 1. it ingests data into indicesm not only data-streams 2. it uses a non-conventional naming scheme for indices --- x-pack/plugins/fleet/common/constants/epm.ts | 1 + ...kage_policies_to_agent_permissions.test.ts | 92 +++++++++++++++++++ .../package_policies_to_agent_permissions.ts | 33 +++++++ 3 files changed, 126 insertions(+) diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index a1d73b452cf720..2635dbc05399ff 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -16,6 +16,7 @@ export const FLEET_ENDPOINT_PACKAGE = 'endpoint'; export const FLEET_APM_PACKAGE = 'apm'; export const FLEET_SYNTHETICS_PACKAGE = 'synthetics'; export const FLEET_KUBERNETES_PACKAGE = 'kubernetes'; +export const FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE = 'profiler_symbolizer'; export const FLEET_CLOUD_SECURITY_POSTURE_PACKAGE = 'cloud_security_posture'; export const FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE = 'kspm'; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts index 1f5ea87c6d6f91..f093b20eaaeb4e 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts @@ -13,6 +13,7 @@ import type { DataStreamMeta } from './package_policies_to_agent_permissions'; import { getDataStreamPrivileges, storedPackagePoliciesToAgentPermissions, + UNIVERSAL_PROFILING_PERMISSIONS, } from './package_policies_to_agent_permissions'; const packageInfoCache = new Map(); @@ -137,6 +138,56 @@ packageInfoCache.set('osquery_manager-0.3.0', { }, }, }); +packageInfoCache.set('profiler_symbolizer-8.8.0-preview', { + format_version: '2.7.0', + name: 'profiler_symbolizer', + title: 'Universal Profiling Symbolizer', + version: '8.8.0-preview', + license: 'basic', + description: + ' Fleet-wide, whole-system, continuous profiling with zero instrumentation. Symbolize native frames.', + type: 'integration', + release: 'beta', + categories: ['monitoring', 'elastic_stack'], + icons: [ + { + src: '/img/logo_profiling_symbolizer.svg', + title: 'logo symbolizer', + size: '32x32', + type: 'image/svg+xml', + }, + ], + owner: { github: 'elastic/profiling' }, + data_streams: [], + latestVersion: '8.8.0-preview', + notice: undefined, + status: 'not_installed', + assets: { + kibana: { + csp_rule_template: [], + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + tag: [], + osquery_pack_asset: [], + osquery_saved_query: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + ml_model: [], + }, + }, +}); describe('storedPackagePoliciesToAgentPermissions()', () => { it('Returns `undefined` if there are no package policies', async () => { @@ -363,6 +414,47 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { }, }); }); + + it('Returns the Universal Profiling permissions for profiler_symbolizer package', async () => { + const packagePolicies: PackagePolicy[] = [ + { + id: 'package-policy-uuid-test-123', + name: 'test-policy', + namespace: '', + enabled: true, + package: { name: 'profiler_symbolizer', version: '8.8.0-preview', title: 'Test Package' }, + inputs: [ + { + type: 'pf-elastic-symbolizer', + enabled: true, + streams: [], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions( + packageInfoCache, + packagePolicies + ); + + expect(permissions).toMatchObject({ + 'package-policy-uuid-test-123': { + indices: [ + { + names: ['profiling-*'], + privileges: UNIVERSAL_PROFILING_PERMISSIONS, + }, + ], + }, + }); + }); }); describe('getDataStreamPrivileges()', () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts index 02c44024421cee..f8cd73901e0d70 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE } from '../../../common/constants'; + import { getNormalizedDataStreams } from '../../../common/services'; import type { @@ -19,6 +21,16 @@ import { pkgToPkgKey } from '../epm/registry'; export const DEFAULT_CLUSTER_PERMISSIONS = ['monitor']; +export const UNIVERSAL_PROFILING_PERMISSIONS = [ + 'auto_configure', + 'read', + 'create_doc', + 'create', + 'write', + 'index', + 'view_index_metadata', +]; + export async function storedPackagePoliciesToAgentPermissions( packageInfoCache: Map, packagePolicies?: PackagePolicy[] @@ -42,6 +54,12 @@ export async function storedPackagePoliciesToAgentPermissions( const pkg = packageInfoCache.get(pkgToPkgKey(packagePolicy.package))!; + // Special handling for Universal Profiling packages, as it does not use data streams _only_, + // but also indices that do not adhere to the convention. + if (pkg.name === FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE) { + return Promise.resolve(universalProfilingPermissions(packagePolicy.id)); + } + const dataStreams = getNormalizedDataStreams(pkg); if (!dataStreams || dataStreams.length === 0) { return [packagePolicy.name, undefined]; @@ -175,3 +193,18 @@ export function getDataStreamPrivileges(dataStream: DataStreamMeta, namespace: s privileges, }; } + +async function universalProfilingPermissions(packagePolicyId: string): Promise<[string, any]> { + const profilingIndexPattern = 'profiling-*'; + return [ + packagePolicyId, + { + indices: [ + { + names: [profilingIndexPattern], + privileges: UNIVERSAL_PROFILING_PERMISSIONS, + }, + ], + }, + ]; +} From 953437f05d26ee42ff70eb128e6646617bf7ba62 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Mon, 24 Apr 2023 20:21:39 +0200 Subject: [PATCH 59/65] [Enterprise Search] Fix wording on content settings page (#155383) ## Summary This updates the content on the content settings page. Screenshot 2023-04-20 at 14 02 02 --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + .../components/settings/settings.tsx | 37 ++++++++++--------- .../components/settings/settings_panel.tsx | 4 +- .../shared/doc_links/doc_links.ts | 3 ++ .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 8 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index d9fbd1dfc85545..dc75ebee9a5ebd 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -153,6 +153,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { licenseManagement: `${ENTERPRISE_SEARCH_DOCS}license-management.html`, machineLearningStart: `${ENTERPRISE_SEARCH_DOCS}machine-learning-start.html`, mailService: `${ENTERPRISE_SEARCH_DOCS}mailer-configuration.html`, + mlDocumentEnrichment: `${ENTERPRISE_SEARCH_DOCS}document-enrichment.html`, start: `${ENTERPRISE_SEARCH_DOCS}start.html`, syncRules: `${ENTERPRISE_SEARCH_DOCS}sync-rules.html`, troubleshootSetup: `${ENTERPRISE_SEARCH_DOCS}troubleshoot-setup.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 92dc64e9166445..efe5e95f238d00 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -138,6 +138,7 @@ export interface DocLinks { readonly licenseManagement: string; readonly machineLearningStart: string; readonly mailService: string; + readonly mlDocumentEnrichment: string; readonly start: string; readonly syncRules: string; readonly troubleshootSetup: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx index a1bbe6c39956f3..840010d3aa219e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx @@ -12,6 +12,8 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiLink, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + import { docLinks } from '../../../shared/doc_links'; import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; @@ -36,6 +38,21 @@ export const Settings: React.FC = () => { }), ]} pageHeader={{ + description: ( + + {i18n.translate('xpack.enterpriseSearch.content.settings.ingestLink', { + defaultMessage: 'ingest pipelines', + })} + + ), + }} + /> + ), pageTitle: i18n.translate('xpack.enterpriseSearch.content.settings.headerTitle', { defaultMessage: 'Content Settings', }), @@ -69,19 +86,12 @@ export const Settings: React.FC = () => { 'xpack.enterpriseSearch.content.settings.contentExtraction.description', { defaultMessage: - 'Allow all ingestion mechanisms on your Enterprise Search deployment to extract searchable content from binary files, like PDFs and Word documents. This setting applies to all new Elasticsearch indices created by an Enterprise Search ingestion mechanism.', + 'Extract searchable content from binary files, like PDFs and Word documents.', } )} label={i18n.translate('xpack.enterpriseSearch.content.settings.contactExtraction.label', { defaultMessage: 'Content extraction', })} - link={ - - {i18n.translate('xpack.enterpriseSearch.content.settings.contactExtraction.link', { - defaultMessage: 'Learn more about content extraction', - })} - - } onChange={() => setPipeline({ ...pipelineState, @@ -105,13 +115,6 @@ export const Settings: React.FC = () => { label={i18n.translate('xpack.enterpriseSearch.content.settings.whitespaceReduction.label', { defaultMessage: 'Whitespace reduction', })} - link={ - - {i18n.translate('xpack.enterpriseSearch.content.settings.whitespaceReduction.link', { - defaultMessage: 'Learn more about whitespace reduction', - })} - - } onChange={() => setPipeline({ ...pipelineState, @@ -139,9 +142,9 @@ export const Settings: React.FC = () => { defaultMessage: 'ML Inference', })} link={ - + {i18n.translate('xpack.enterpriseSearch.content.settings.mlInference.link', { - defaultMessage: 'Learn more about content extraction', + defaultMessage: 'Learn more about document enrichment with ML', })} } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx index fc0f3cca3e06c8..673861d80e6efd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; interface SettingsPanelProps { description: string; label: string; - link: React.ReactNode; + link?: React.ReactNode; onChange: (event: EuiSwitchEvent) => void; title: string; value: boolean; @@ -61,7 +61,7 @@ export const SettingsPanel: React.FC = ({ - {link} + {link && {link}} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index 938aaa88d1bdf2..cb6663eebf6abc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -92,6 +92,7 @@ class DocLinks { public languageClients: string; public licenseManagement: string; public machineLearningStart: string; + public mlDocumentEnrichment: string; public pluginsIngestAttachment: string; public queryDsl: string; public searchUIAppSearch: string; @@ -219,6 +220,7 @@ class DocLinks { this.languageClients = ''; this.licenseManagement = ''; this.machineLearningStart = ''; + this.mlDocumentEnrichment = ''; this.pluginsIngestAttachment = ''; this.queryDsl = ''; this.searchUIAppSearch = ''; @@ -340,6 +342,7 @@ class DocLinks { this.languageClients = docLinks.links.enterpriseSearch.languageClients; this.licenseManagement = docLinks.links.enterpriseSearch.licenseManagement; this.machineLearningStart = docLinks.links.enterpriseSearch.machineLearningStart; + this.mlDocumentEnrichment = docLinks.links.enterpriseSearch.mlDocumentEnrichment; this.pluginsIngestAttachment = docLinks.links.plugins.ingestAttachment; this.queryDsl = docLinks.links.query.queryDsl; this.searchUIAppSearch = docLinks.links.searchUI.appSearch; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d09b84345903e4..f6610dba543b2a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12711,7 +12711,6 @@ "xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "Statut", "xpack.enterpriseSearch.content.settings.breadcrumb": "Paramètres", "xpack.enterpriseSearch.content.settings.contactExtraction.label": "Extraction du contenu", - "xpack.enterpriseSearch.content.settings.contactExtraction.link": "En savoir plus sur l'extraction de contenu", "xpack.enterpriseSearch.content.settings.contentExtraction.description": "Autoriser tous les mécanismes d'ingestion de votre déploiement Enterprise Search à extraire le contenu interrogeable des fichiers binaires tels que les documents PDF et Word. Ce paramètre s'applique à tous les nouveaux index Elasticsearch créés par un mécanisme d'ingestion Enterprise Search.", "xpack.enterpriseSearch.content.settings.contentExtraction.descriptionTwo": "Vous pouvez également activer ou désactiver cette fonctionnalité pour un index spécifique sur la page de configuration de l'index.", "xpack.enterpriseSearch.content.settings.contentExtraction.title": "Extraction de contenu de l'ensemble du déploiement", @@ -12725,7 +12724,6 @@ "xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "Réduction d'espaces sur l'ensemble du déploiement", "xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "La réduction d'espaces supprimera le contenu de texte intégral des espaces par défaut.", "xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "Réduction d'espaces", - "xpack.enterpriseSearch.content.settings.whitespaceReduction.link": "En savoir plus sur la réduction d'espaces", "xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "Supprimer le document", "xpack.enterpriseSearch.content.shared.result.header.metadata.title": "Métadonnées du document", "xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "Inclure tout le reste à partir de cette source", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0fc77aa25087d8..91baf79cfaaa0a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12710,7 +12710,6 @@ "xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "ステータス", "xpack.enterpriseSearch.content.settings.breadcrumb": "設定", "xpack.enterpriseSearch.content.settings.contactExtraction.label": "コンテンツ抽出", - "xpack.enterpriseSearch.content.settings.contactExtraction.link": "コンテンツ抽出の詳細", "xpack.enterpriseSearch.content.settings.contentExtraction.description": "エンタープライズ サーチデプロイですべてのインジェストメソッドで、PDFやWordドキュメントなどのバイナリファイルから検索可能なコンテンツを抽出できます。この設定は、エンタープライズ サーチインジェストメカニズムで作成されたすべての新しいElasticsearchインデックスに適用されます。", "xpack.enterpriseSearch.content.settings.contentExtraction.descriptionTwo": "インデックスの構成ページで、特定のインデックスに対して、この機能を有効化または無効化することもできます。", "xpack.enterpriseSearch.content.settings.contentExtraction.title": "デプロイレベルのコンテンツ抽出", @@ -12724,7 +12723,6 @@ "xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "デプロイレベルの空白削除", "xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "空白削除では、デフォルトで全文コンテンツから空白を削除します。", "xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "空白削除", - "xpack.enterpriseSearch.content.settings.whitespaceReduction.link": "空白削除の詳細", "xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "ドキュメントを削除", "xpack.enterpriseSearch.content.shared.result.header.metadata.title": "ドキュメントメタデータ", "xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "このソースの他のすべての項目を含める", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1ce46a22423078..ed0f6a5c2fedb9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12711,7 +12711,6 @@ "xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "状态", "xpack.enterpriseSearch.content.settings.breadcrumb": "设置", "xpack.enterpriseSearch.content.settings.contactExtraction.label": "内容提取", - "xpack.enterpriseSearch.content.settings.contactExtraction.link": "详细了解内容提取", "xpack.enterpriseSearch.content.settings.contentExtraction.description": "允许您的 Enterprise Search 部署上的所有采集机制从 PDF 和 Word 文档等二进制文件中提取可搜索内容。此设置适用于由 Enterprise Search 采集机制创建的所有新 Elasticsearch 索引。", "xpack.enterpriseSearch.content.settings.contentExtraction.descriptionTwo": "您还可以在索引的配置页面针对特定索引启用或禁用此功能。", "xpack.enterpriseSearch.content.settings.contentExtraction.title": "部署广泛内容提取", @@ -12725,7 +12724,6 @@ "xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "部署广泛的空白缩减", "xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "默认情况下,空白缩减将清除空白的全文本内容。", "xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "空白缩减", - "xpack.enterpriseSearch.content.settings.whitespaceReduction.link": "详细了解空白缩减", "xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "删除文档", "xpack.enterpriseSearch.content.shared.result.header.metadata.title": "文档元数据", "xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "包括来自此源的所有其他内容", From 4382e1cf3227e966995c2486043d34f1b875d7d9 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Mon, 24 Apr 2023 15:14:30 -0400 Subject: [PATCH 60/65] [ResponseOps] adds mustache lambdas and array.asJSON (#150572) partially resolves some issues in https://github.com/elastic/kibana/issues/84217 Adds Mustache lambdas for alerting actions to format dates with `{{#FormatDate}}`, evaluate math expressions with `{{#EvalMath}}`, and provide easier JSON formatting with `{{#ParseHjson}}` and a new `asJSON` property added to arrays. --- .../server/lib/mustache_lambdas.test.ts | 201 +++++++++ .../actions/server/lib/mustache_lambdas.ts | 114 +++++ .../server/lib/mustache_renderer.test.ts | 11 + .../actions/server/lib/mustache_renderer.ts | 9 +- x-pack/plugins/actions/tsconfig.json | 3 +- .../server/slack_simulation.ts | 2 +- .../server/webhook_simulation.ts | 4 +- .../plugins/alerts/server/alert_types.ts | 1 + .../alerting/group4/mustache_templates.ts | 421 ++++++++---------- 9 files changed, 534 insertions(+), 232 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts create mode 100644 x-pack/plugins/actions/server/lib/mustache_lambdas.ts diff --git a/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts b/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts new file mode 100644 index 00000000000000..6f67c4dd39ea8e --- /dev/null +++ b/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import dedent from 'dedent'; + +import { renderMustacheString } from './mustache_renderer'; + +describe('mustache lambdas', () => { + describe('FormatDate', () => { + it('date with defaults is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 03:52pm'); + }); + + it('date with a time zone is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} ; America/New_York {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 10:52am'); + }); + + it('date with a format is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} ;; dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual( + 'Tuesday Nov 29th 2022 15:52:44.000' + ); + }); + + it('date with a format and timezone is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}};America/New_York;dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'Tuesday Nov 29th 2022 10:52:44.000' + ); + }); + + it('empty date produces error', () => { + const timeStamp = ''; + const template = dedent` + {{#FormatDate}} {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}} {{/FormatDate}}": date is empty' + ); + }); + + it('invalid date produces error', () => { + const timeStamp = 'this is not a d4t3'; + const template = dedent` + {{#FormatDate}}{{timeStamp}}{{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}}{{timeStamp}}{{/FormatDate}}": invalid date "this is not a d4t3"' + ); + }); + + it('invalid timezone produces error', () => { + const timeStamp = '2023-04-10T23:52:39'; + const template = dedent` + {{#FormatDate}}{{timeStamp}};NotATime Zone!{{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}}{{timeStamp}};NotATime Zone!{{/FormatDate}}": unknown timeZone value "NotATime Zone!"' + ); + }); + + it('invalid format produces error', () => { + const timeStamp = '2023-04-10T23:52:39'; + const template = dedent` + {{#FormatDate}}{{timeStamp}};;garbage{{/FormatDate}} + `.trim(); + + // not clear how to force an error, it pretty much does something with + // ANY string + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'gamrbamg2' // a => am/pm (so am here); e => day of week + ); + }); + }); + + describe('EvalMath', () => { + it('math is successful', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const template = dedent` + {{#EvalMath}} 1 + 0 {{/EvalMath}} + {{#EvalMath}} 1 + context.a.b {{/EvalMath}} + {{#context}} + {{#EvalMath}} 1 + c.d {{/EvalMath}} + {{/context}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(result).toEqual(`1\n2\n3\n`); + }); + + it('invalid expression produces error', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const template = dedent` + {{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(result).toEqual( + `error rendering mustache template "{{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}}": error evaluating tinymath expression ") 1 ++++ 0 (": Failed to parse expression. Expected "(", function, literal, or whitespace but ")" found.` + ); + }); + }); + + describe('ParseHJson', () => { + it('valid Hjson is successful', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const hjson = ` + { + # specify rate in requests/second (because comments are helpful!) + rate: 1000 + + a: {{context.a}} + a_b: {{context.a.b}} + c: {{context.c}} + c_d: {{context.c.d}} + + # list items can be separated by lines, or commas, and trailing + # commas permitted + list: [ + 1 2 + 3 + 4,5,6, + ] + }`; + const template = dedent` + {{#ParseHjson}} ${hjson} {{/ParseHjson}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(JSON.parse(result)).toMatchInlineSnapshot(` + Object { + "a": Object { + "b": 1, + }, + "a_b": 1, + "c": Object { + "d": 2, + }, + "c_d": 2, + "list": Array [ + "1 2", + 3, + 4, + 5, + 6, + ], + "rate": 1000, + } + `); + }); + + it('renders an error message on parse errors', () => { + const template = dedent` + {{#ParseHjson}} [1,2,3,,] {{/ParseHjson}} + `.trim(); + + const result = renderMustacheString(template, {}, 'none'); + expect(result).toMatch(/^error rendering mustache template .*/); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/mustache_lambdas.ts b/x-pack/plugins/actions/server/lib/mustache_lambdas.ts new file mode 100644 index 00000000000000..62ba5621e0e1e0 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/mustache_lambdas.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as tinymath from '@kbn/tinymath'; +import { parse as hjsonParse } from 'hjson'; + +import moment from 'moment-timezone'; + +type Variables = Record; + +const DefaultDateTimeZone = 'UTC'; +const DefaultDateFormat = 'YYYY-MM-DD hh:mma'; + +export function getMustacheLambdas(): Variables { + return getLambdas(); +} + +const TimeZoneSet = new Set(moment.tz.names()); + +type RenderFn = (text: string) => string; + +function getLambdas() { + return { + EvalMath: () => + // mustache invokes lamdas with `this` set to the current "view" (variables) + function (this: Variables, text: string, render: RenderFn) { + return evalMath(this, render(text.trim())); + }, + ParseHjson: () => + function (text: string, render: RenderFn) { + return parseHjson(render(text.trim())); + }, + FormatDate: () => + function (text: string, render: RenderFn) { + const dateString = render(text.trim()).trim(); + return formatDate(dateString); + }, + }; +} + +function evalMath(vars: Variables, o: unknown): string { + const expr = `${o}`; + try { + const result = tinymath.evaluate(expr, vars); + return `${result}`; + } catch (err) { + throw new Error(`error evaluating tinymath expression "${expr}": ${err.message}`); + } +} + +function parseHjson(o: unknown): string { + const hjsonObject = `${o}`; + let object: unknown; + + try { + object = hjsonParse(hjsonObject); + } catch (err) { + throw new Error(`error parsing Hjson "${hjsonObject}": ${err.message}`); + } + + return JSON.stringify(object); +} + +function formatDate(dateString: unknown): string { + const { date, timeZone, format } = splitDateString(`${dateString}`); + + if (date === '') { + throw new Error(`date is empty`); + } + + if (isNaN(new Date(date).valueOf())) { + throw new Error(`invalid date "${date}"`); + } + + let mDate: moment.Moment; + try { + mDate = moment(date); + if (!mDate.isValid()) { + throw new Error(`date is invalid`); + } + } catch (err) { + throw new Error(`error evaluating moment date "${date}": ${err.message}`); + } + + if (!TimeZoneSet.has(timeZone)) { + throw new Error(`unknown timeZone value "${timeZone}"`); + } + + try { + mDate.tz(timeZone); + } catch (err) { + throw new Error(`error evaluating moment timeZone "${timeZone}": ${err.message}`); + } + + try { + return mDate.format(format); + } catch (err) { + throw new Error(`error evaluating moment format "${format}": ${err.message}`); + } +} + +function splitDateString(dateString: string) { + const parts = dateString.split(';', 3).map((s) => s.trim()); + const [date = '', timeZone = '', format = ''] = parts; + return { + date, + timeZone: timeZone || DefaultDateTimeZone, + format: format || DefaultDateFormat, + }; +} diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts index 964a793d8f81c1..3a02ce0d1a9838 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts @@ -58,6 +58,12 @@ describe('mustache_renderer', () => { expect(renderMustacheString('{{f.g}}', variables, escape)).toBe('3'); expect(renderMustacheString('{{f.h}}', variables, escape)).toBe(''); expect(renderMustacheString('{{i}}', variables, escape)).toBe('42,43,44'); + + if (escape === 'markdown') { + expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('\\[42,43,44\\]'); + } else { + expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('[42,43,44]'); + } }); } @@ -339,6 +345,11 @@ describe('mustache_renderer', () => { const expected = '1 - {"c":2,"d":[3,4]} -- 5,{"f":6,"g":7}'; expect(renderMustacheString('{{a}} - {{b}} -- {{e}}', deepVariables, 'none')).toEqual(expected); + + expect(renderMustacheString('{{e}}', deepVariables, 'none')).toEqual('5,{"f":6,"g":7}'); + expect(renderMustacheString('{{e.asJSON}}', deepVariables, 'none')).toEqual( + '[5,{"f":6,"g":7}]' + ); }); describe('converting dot variables', () => { diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.ts index fc4381fa0c9c3d..37713167e9a341 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.ts @@ -7,8 +7,10 @@ import Mustache from 'mustache'; import { isString, isPlainObject, cloneDeepWith, merge } from 'lodash'; +import { getMustacheLambdas } from './mustache_lambdas'; export type Escape = 'markdown' | 'slack' | 'json' | 'none'; + type Variables = Record; // return a rendered mustache template with no escape given the specified variables and escape @@ -25,11 +27,13 @@ export function renderMustacheStringNoEscape(string: string, variables: Variable // return a rendered mustache template given the specified variables and escape export function renderMustacheString(string: string, variables: Variables, escape: Escape): string { const augmentedVariables = augmentObjectVariables(variables); + const lambdas = getMustacheLambdas(); + const previousMustacheEscape = Mustache.escape; Mustache.escape = getEscape(escape); try { - return Mustache.render(`${string}`, augmentedVariables); + return Mustache.render(`${string}`, { ...lambdas, ...augmentedVariables }); } catch (err) { // log error; the mustache code does not currently leak variables return `error rendering mustache template "${string}": ${err.message}`; @@ -98,6 +102,9 @@ function addToStringDeep(object: unknown): void { // walk arrays, but don't add a toString() as mustache already does something if (Array.isArray(object)) { + // instead, add an asJSON() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (object as any).asJSON = () => JSON.stringify(object); object.forEach((element) => addToStringDeep(element)); return; } diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index 77ef11e88bfe30..8c253cb644feec 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -33,7 +33,8 @@ "@kbn/logging-mocks", "@kbn/core-elasticsearch-client-server-mocks", "@kbn/safer-lodash-set", - "@kbn/core-http-server-mocks" + "@kbn/core-http-server-mocks", + "@kbn/tinymath", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts index f1a67c568b67fe..eee078591a3a7a 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts @@ -35,7 +35,7 @@ export async function initPlugin() { } // store a message that was posted to be remembered - const match = text.match(/^message (.*)$/); + const match = text.match(/^message ([\S\s]*)$/); if (match) { messages.push(match[1]); response.statusCode = 200; diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts index f3f0aa8f6469bf..baa6ee80a8e539 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts @@ -79,7 +79,7 @@ function createServerCallback() { } // store a payload that was posted to be remembered - const match = data.match(/^payload (.*)$/); + const match = data.match(/^payload ([\S\s]*)$/); if (match) { payloads.push(match[1]); response.statusCode = 200; @@ -89,6 +89,8 @@ function createServerCallback() { response.statusCode = 400; response.end(`unexpected body ${data}`); + // eslint-disable-next-line no-console + console.log(`webhook simulator received unexpected body: ${data}`); return; }); } else { diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts index 60e7e82966864d..6d716b5d3c235f 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts @@ -47,6 +47,7 @@ export const DeepContextVariables = { arrayI: [44, 45], nullJ: null, undefinedK: undefined, + dateL: '2023-04-20T04:13:17.858Z', }; function getAlwaysFiringAlertType() { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts index 1eb7d99b93bda3..764dd728b13479 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts @@ -14,13 +14,16 @@ import http from 'http'; import getPort from 'get-port'; -import { URL, format as formatUrl } from 'url'; import axios from 'axios'; import expect from '@kbn/expect'; import { getWebhookServer, getSlackServer } from '@kbn/actions-simulators-plugin/server/plugin'; import { Spaces } from '../../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { + getUrlPrefix, + getTestRuleData as getCoreTestRuleData, + ObjectRemover, +} from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -32,8 +35,10 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon const objectRemover = new ObjectRemover(supertest); let webhookSimulatorURL: string = ''; let webhookServer: http.Server; + let webhookConnector: any; let slackSimulatorURL: string = ''; let slackServer: http.Server; + let slackConnector: any; before(async () => { let availablePort: number; @@ -42,6 +47,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon availablePort = await getPort({ port: 9000 }); webhookServer.listen(availablePort); webhookSimulatorURL = `http://localhost:${availablePort}`; + webhookConnector = await createWebhookConnector(webhookSimulatorURL); slackServer = await getSlackServer(); availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); @@ -49,6 +55,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon slackServer.listen(availablePort); } slackSimulatorURL = `http://localhost:${availablePort}`; + slackConnector = await createSlackConnector(slackSimulatorURL); }); after(async () => { @@ -57,219 +64,177 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon slackServer.close(); }); - it('should handle escapes in webhook', async () => { - const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing mustache escapes for webhook', - connector_type_id: '.webhook', - secrets: {}, - config: { - headers: { - 'Content-Type': 'text/plain', - }, - url, + describe('escaping', () => { + it('should handle escapes in webhook', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const EscapableStrings + const template = '{{context.escapableDoubleQuote}} -- {{context.escapableLineFeed}}'; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be(`\\"double quote\\" -- line\\nfeed`); + }); + + it('should handle escapes in slack', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const EscapableStrings + const template = + '{{context.escapableBacktic}} -- {{context.escapableBold}} -- {{context.escapableBackticBold}} -- {{context.escapableHtml}}'; + + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const EscapableStrings - const varsTemplate = '{{context.escapableDoubleQuote}} -- {{context.escapableLineFeed}}'; - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for webhook', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - body: `payload {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be("back'tic -- `*bold*` -- `'*bold*'` -- <&>"); + }); + + it('should handle context variable object expansion', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = '{{context.deep}}'; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be( + '{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null,"dateL":"2023-04-20T04:13:17.858Z"}' ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(webhookSimulatorURL, createdAlert.id) - ); - expect(body).to.be(`\\"double quote\\" -- line\\nfeed`); - }); - - it('should handle escapes in slack', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: "testing backtic'd mustache escapes for slack", - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + }); + + it('should render kibanaBaseUrl as empty string since not configured', async () => { + const template = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"'; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const EscapableStrings - const varsTemplate = - '{{context.escapableBacktic}} -- {{context.escapableBold}} -- {{context.escapableBackticBold}} -- {{context.escapableHtml}}'; + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be('kibanaBaseUrl: ""'); + }); - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for slack', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be("back'tic -- `*bold*` -- `'*bold*'` -- <&>"); - }); - - it('should handle context variable object expansion', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing context variable expansion', - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + it('should render action variables in rule action', async () => { + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{rule.id}} - old id variable: {{alertId}}, new id variable: {{rule.id}}, old name variable: {{alertName}}, new name variable: {{rule.name}}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const DeepContextVariables - const varsTemplate = '{{context.deep}}'; - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing context variable expansion', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true, true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be( + `old id variable: ${rule.id}, new id variable: ${rule.id}, old name variable: ${rule.name}, new name variable: ${rule.name}` ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be( - '{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null}' - ); + }); }); - it('should render kibanaBaseUrl as empty string since not configured', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing context variable expansion', - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + describe('lambdas', () => { + it('should handle ParseHjson', async () => { + const template = `{{#ParseHjson}} { + ruleId: {{rule.id}} + ruleName: {{rule.name}} + } {{/ParseHjson}}`; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - const varsTemplate = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"'; + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be(`{"ruleId":"${rule.id}","ruleName":"testing mustache templates"}`); + }); + + it('should handle asJSON', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep.objectA}} + {{{arrayC}}} {{{arrayC.asJSON}}} + {{/context.deep.objectA}} + `; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + const expected1 = `{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}`; + const expected2 = `[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}]`; + expect(body.trim()).to.be(`${expected1} ${expected2}`); + }); + + it('should handle EvalMath', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep}}avg({{arrayI.0}}, {{arrayI.1}})/100 => {{#EvalMath}} + round((arrayI[0] + arrayI[1]) / 2 / 100, 2) + {{/EvalMath}}{{/context.deep}}`; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be(`avg(44, 45)/100 => 0.45`); + }); + + it('should handle FormatDate', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep}}{{#FormatDate}} + {{{dateL}}} ; America/New_York; dddd MMM Do YYYY HH:mm:ss + {{/FormatDate}}{{/context.deep}}`; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body.trim()).to.be(`Thursday Apr 20th 2023 00:13:17`); + }); + }); - const alertResponse = await supertest + async function createRule(action: any) { + const ruleResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing context variable kibanaBaseUrl', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true, true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be('kibanaBaseUrl: ""'); - }); + .send(getTestRuleData({ actions: [action] })); + expect(ruleResponse.status).to.eql(200); + const rule = ruleResponse.body; + objectRemover.add(Spaces.space1.id, rule.id, 'rule', 'alerting'); + + return rule; + } - it('should render action variables in rule action', async () => { - const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); - const actionResponse = await supertest + async function createWebhookConnector(url: string) { + const createResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'test') .send({ - name: 'testing action variable rendering', + name: 'testing mustache for webhook', connector_type_id: '.webhook', secrets: {}, config: { @@ -279,42 +244,30 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon url, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); + expect(createResponse.status).to.eql(200); + const connector = createResponse.body; + objectRemover.add(Spaces.space1.id, connector.id, 'connector', 'actions'); - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for webhook', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - body: `payload {{rule.id}} - old id variable: {{alertId}}, new id variable: {{rule.id}}, old name variable: {{alertName}}, new name variable: {{rule.name}}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(webhookSimulatorURL, createdAlert.id) - ); - expect(body).to.be( - `old id variable: ${createdAlert.id}, new id variable: ${createdAlert.id}, old name variable: ${createdAlert.name}, new name variable: ${createdAlert.name}` - ); - }); + return connector; + } + + async function createSlackConnector(url: string) { + const createResponse = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'test') + .send({ + name: 'testing mustache for slack', + connector_type_id: '.slack', + secrets: { + webhookUrl: url, + }, + }); + expect(createResponse.status).to.eql(200); + const connector = createResponse.body; + objectRemover.add(Spaces.space1.id, connector.id, 'connector', 'actions'); + + return connector; + } }); async function waitForActionBody(url: string, id: string): Promise { @@ -322,7 +275,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon expect(response.status).to.eql(200); for (const datum of response.data) { - const match = datum.match(/^(.*) - (.*)$/); + const match = datum.match(/^(.*) - ([\S\s]*)$/); if (match == null) continue; if (match[1] === id) return match[2]; @@ -331,3 +284,15 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon throw new Error(`no action body posted yet for id ${id}`); } } + +function getTestRuleData(overrides: any) { + const defaults = { + name: 'testing mustache templates', + rule_type_id: 'test.patternFiring', + params: { + pattern: { instance: [true] }, + }, + }; + + return getCoreTestRuleData({ ...overrides, ...defaults }); +} From e46cb1ab8a98af3cbad8c6affcd3477cea9abc9d Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Mon, 24 Apr 2023 14:16:43 -0500 Subject: [PATCH 61/65] [Enterprise Search][Search Applications] introduce content page (#155632) ## Summary Introduced a Content page for Search Application that combined the Indices and Schema pages into a single page with tabs ### Screenshots image image image ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../engine/engine_connect/engine_connect.tsx | 4 +- .../components/engine/engine_indices.tsx | 206 +++++++--------- .../components/engine/engine_schema.tsx | 227 ++++++++---------- .../components/engine/engine_view.tsx | 12 +- .../engine/search_application_content.tsx | 156 ++++++++++++ .../enterprise_search_content/routes.ts | 8 +- .../applications/shared/layout/nav.test.tsx | 11 +- .../public/applications/shared/layout/nav.tsx | 19 +- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 11 files changed, 372 insertions(+), 283 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/search_application_content.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx index 2dd55304b60353..a6c29285f4f660 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { generateEncodedPath } from '../../../../shared/encode_path_params'; import { KibanaLogic } from '../../../../shared/kibana'; import { - SEARCH_APPLICATION_CONNECT_PATH, + SEARCH_APPLICATION_CONTENT_PATH, EngineViewTabs, SearchApplicationConnectTabs, } from '../../../routes'; @@ -55,7 +55,7 @@ export const EngineConnect: React.FC = () => { }>(); const onTabClick = (tab: SearchApplicationConnectTabs) => () => { KibanaLogic.values.navigateToUrl( - generateEncodedPath(SEARCH_APPLICATION_CONNECT_PATH, { + generateEncodedPath(SEARCH_APPLICATION_CONTENT_PATH, { engineName, connectTabId: tab, }) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx index 4bcd3fcf4f2158..285846f7ccb1c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx @@ -11,7 +11,6 @@ import { useActions, useValues } from 'kea'; import { EuiBasicTableColumn, - EuiButton, EuiCallOut, EuiConfirmModal, EuiIcon, @@ -32,24 +31,15 @@ import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry/telemetry_logic'; -import { SEARCH_INDEX_PATH, EngineViewTabs } from '../../routes'; +import { SEARCH_INDEX_PATH } from '../../routes'; -import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template'; - -import { AddIndicesFlyout } from './add_indices_flyout'; import { EngineIndicesLogic } from './engine_indices_logic'; -const pageTitle = i18n.translate('xpack.enterpriseSearch.content.engine.indices.pageTitle', { - defaultMessage: 'Indices', -}); - export const EngineIndices: React.FC = () => { const subduedBackground = useEuiBackgroundColor('subdued'); const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic); - const { engineData, engineName, isLoadingEngine, addIndicesFlyoutOpen } = - useValues(EngineIndicesLogic); - const { removeIndexFromEngine, openAddIndicesFlyout, closeAddIndicesFlyout } = - useActions(EngineIndicesLogic); + const { engineData } = useValues(EngineIndicesLogic); + const { removeIndexFromEngine } = useActions(EngineIndicesLogic); const { navigateToUrl } = useValues(KibanaLogic); const [removeIndexConfirm, setConfirmRemoveIndex] = useState(null); @@ -177,116 +167,92 @@ export const EngineIndices: React.FC = () => { }, ]; return ( - + {hasUnknownIndices && ( + <> + - {i18n.translate('xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', { - defaultMessage: 'Add new indices', - })} - , - ], - }} - engineName={engineName} - > - <> - {hasUnknownIndices && ( - <> - + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.unknownIndicesCallout.description', + { + defaultMessage: + 'Some data might be unreachable from this search application. Check for any pending operations or errors on affected indices, or remove indices that should no longer be used by this search application.', + } )} - > -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.unknownIndicesCallout.description', - { - defaultMessage: - 'Some data might be unreachable from this search application. Check for any pending operations or errors on affected indices, or remove those that should no longer be used by this search application.', - } - )} -

-
- - - )} - { - if (index.health === 'unknown') { - return { style: { backgroundColor: subduedBackground } }; - } +

+
+ + + )} + { + if (index.health === 'unknown') { + return { style: { backgroundColor: subduedBackground } }; + } - return {}; - }} - search={{ - box: { - incremental: true, - placeholder: i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.searchPlaceholder', - { defaultMessage: 'Filter indices' } - ), - schema: true, - }, + return {}; + }} + search={{ + box: { + incremental: true, + placeholder: i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.searchPlaceholder', + { defaultMessage: 'Filter indices' } + ), + schema: true, + }, + }} + pagination + sorting + /> + {removeIndexConfirm !== null && ( + setConfirmRemoveIndex(null)} + onConfirm={() => { + removeIndexFromEngine(removeIndexConfirm); + setConfirmRemoveIndex(null); + sendEnterpriseSearchTelemetry({ + action: 'clicked', + metric: 'entSearchContent-engines-indices-removeIndexConfirm', + }); }} - pagination - sorting - /> - {removeIndexConfirm !== null && ( - setConfirmRemoveIndex(null)} - onConfirm={() => { - removeIndexFromEngine(removeIndexConfirm); - setConfirmRemoveIndex(null); - sendEnterpriseSearchTelemetry({ - action: 'clicked', - metric: 'entSearchContent-engines-indices-removeIndexConfirm', - }); - }} - title={i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title', - { defaultMessage: 'Remove this index from the Search Application' } - )} - buttonColor="danger" - cancelButtonText={CANCEL_BUTTON_LABEL} - confirmButtonText={i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text', - { - defaultMessage: 'Yes, Remove This Index', - } - )} - defaultFocusedButton="confirm" - maxWidth - > - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description', - { - defaultMessage: - "This won't delete the index. You may add it back to this search application at a later time.", - } - )} -

-
-
- )} - {addIndicesFlyoutOpen && } - -
+ title={i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title', + { defaultMessage: 'Remove this index from the search application' } + )} + buttonColor="danger" + cancelButtonText={CANCEL_BUTTON_LABEL} + confirmButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text', + { + defaultMessage: 'Yes, Remove This Index', + } + )} + defaultFocusedButton="confirm" + maxWidth + > + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description', + { + defaultMessage: + "This won't delete the index. You may add it back to this search application at a later time.", + } + )} +

+
+ + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx index 4ceba9cccb1424..fd10624188ff0c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx @@ -42,8 +42,7 @@ import { docLinks } from '../../../shared/doc_links'; import { generateEncodedPath } from '../../../shared/encode_path_params'; import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { EngineViewTabs, SEARCH_INDEX_TAB_PATH } from '../../routes'; -import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template'; +import { SEARCH_INDEX_TAB_PATH } from '../../routes'; import { EngineIndicesLogic } from './engine_indices_logic'; @@ -153,10 +152,6 @@ const SchemaFieldDetails: React.FC<{ schemaField: SchemaField }> = ({ schemaFiel ); }; -const pageTitle = i18n.translate('xpack.enterpriseSearch.content.engine.schema.pageTitle', { - defaultMessage: 'Schema', -}); - export const EngineSchema: React.FC = () => { const { engineName } = useValues(EngineIndicesLogic); const [onlyShowConflicts, setOnlyShowConflicts] = useState(false); @@ -348,125 +343,115 @@ export const EngineSchema: React.FC = () => { ); return ( - - <> - - - - - - {i18n.translate('xpack.enterpriseSearch.content.engine.schema.filters.label', { - defaultMessage: 'Filter By', - })} - - - setIsFilterByPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downCenter" + <> + + + + + + {i18n.translate('xpack.enterpriseSearch.content.engine.schema.filters.label', { + defaultMessage: 'Filter By', + })} + + + setIsFilterByPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downCenter" + > + setSelectedEsFieldTypes(options)} > - ( +
+ {search} + {list} +
+ )} +
+ + + setSelectedEsFieldTypes(esFieldTypes)} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.clearAll', { - defaultMessage: 'Filter list ', + defaultMessage: 'Clear all ', } - ), - }} - options={selectedEsFieldTypes} - onChange={(options) => setSelectedEsFieldTypes(options)} - > - {(list, search) => ( -
- {search} - {list} -
- )} -
- - - setSelectedEsFieldTypes(esFieldTypes)} - > - {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.clearAll', - { - defaultMessage: 'Clear all ', - } - )} - - - -
-
-
+ )} + +
+ +
+
- - - {totalConflictsHiddenByTypeFilters > 0 && ( - - } - color="danger" - iconType="iInCircle" - > -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.subTitle', - { - defaultMessage: - 'In order to see all field conflicts you must clear your field filters', - } - )} -

- setSelectedEsFieldTypes(esFieldTypes)}> - {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.clearFilters', - { - defaultMessage: 'Clear filters ', - } - )} - -
- )}
- -
+ + + {totalConflictsHiddenByTypeFilters > 0 && ( + + } + color="danger" + iconType="iInCircle" + > +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.subTitle', + { + defaultMessage: + 'In order to see all field conflicts you must clear your field filters', + } + )} +

+ setSelectedEsFieldTypes(esFieldTypes)}> + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.clearFilters', + { + defaultMessage: 'Clear filters ', + } + )} + +
+ )} + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx index 35e56f825d33ad..6bbfe9b1de9673 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx @@ -17,9 +17,11 @@ import { Status } from '../../../../../common/types/api'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_PATH, + SEARCH_APPLICATION_CONTENT_PATH, SEARCH_APPLICATION_CONNECT_PATH, EngineViewTabs, SearchApplicationConnectTabs, + SearchApplicationContentTabs, } from '../../routes'; import { DeleteEngineModal } from '../engines/delete_engine_modal'; @@ -27,11 +29,10 @@ import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_temp import { EngineConnect } from './engine_connect/engine_connect'; import { EngineError } from './engine_error'; -import { EngineIndices } from './engine_indices'; -import { EngineSchema } from './engine_schema'; import { EngineSearchPreview } from './engine_search_preview/engine_search_preview'; import { EngineViewLogic } from './engine_view_logic'; import { EngineHeaderDocsAction } from './header_docs_action'; +import { SearchApplicationContent } from './search_application_content'; export const EngineView: React.FC = () => { const { fetchEngine, closeDeleteEngineModal } = useActions(EngineViewLogic); @@ -74,8 +75,11 @@ export const EngineView: React.FC = () => { path={`${ENGINE_PATH}/${EngineViewTabs.PREVIEW}`} component={EngineSearchPreview} /> - - + + { + switch (tabId) { + case SearchApplicationContentTabs.INDICES: + return INDICES_TAB_TITLE; + case SearchApplicationContentTabs.SCHEMA: + return SCHEMA_TAB_TITLE; + default: + return tabId; + } +}; + +const ContentTabs: string[] = Object.values(SearchApplicationContentTabs); + +export const SearchApplicationContent = () => { + const { engineName, isLoadingEngine } = useValues(EngineViewLogic); + const { addIndicesFlyoutOpen } = useValues(EngineIndicesLogic); + const { closeAddIndicesFlyout, openAddIndicesFlyout } = useActions(EngineIndicesLogic); + const { contentTabId = SearchApplicationContentTabs.INDICES } = useParams<{ + contentTabId?: string; + }>(); + + if (!ContentTabs.includes(contentTabId)) { + return ( + + + + ); + } + + const onTabClick = (tab: SearchApplicationContentTabs) => () => { + KibanaLogic.values.navigateToUrl( + generateEncodedPath(SEARCH_APPLICATION_CONTENT_PATH, { + contentTabId: tab, + engineName, + }) + ); + }; + + return ( + + KibanaLogic.values.navigateToUrl( + generateEncodedPath(ENGINE_PATH, { + engineName, + }) + ), + text: ( + <> + {engineName} + + ), + }, + ], + pageTitle, + rightSideItems: [ + + {i18n.translate('xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', { + defaultMessage: 'Add new indices', + })} + , + ], + tabs: [ + { + isSelected: contentTabId === SearchApplicationContentTabs.INDICES, + label: INDICES_TAB_TITLE, + onClick: onTabClick(SearchApplicationContentTabs.INDICES), + }, + { + isSelected: contentTabId === SearchApplicationContentTabs.SCHEMA, + label: SCHEMA_TAB_TITLE, + onClick: onTabClick(SearchApplicationContentTabs.SCHEMA), + }, + ], + }} + engineName={engineName} + > + {contentTabId === SearchApplicationContentTabs.INDICES && } + {contentTabId === SearchApplicationContentTabs.SCHEMA && } + {addIndicesFlyoutOpen && } + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts index ea5672222014fd..7b5bc7bf286f58 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts @@ -29,8 +29,7 @@ export const ENGINES_PATH = `${ROOT_PATH}engines`; export enum EngineViewTabs { PREVIEW = 'preview', - INDICES = 'indices', - SCHEMA = 'schema', + CONTENT = 'content', CONNECT = 'connect', } export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; @@ -40,5 +39,10 @@ export const SEARCH_APPLICATION_CONNECT_PATH = `${ENGINE_PATH}/${EngineViewTabs. export enum SearchApplicationConnectTabs { API = 'api', } +export const SEARCH_APPLICATION_CONTENT_PATH = `${ENGINE_PATH}/${EngineViewTabs.CONTENT}/:contentTabId`; +export enum SearchApplicationContentTabs { + INDICES = 'indices', + SCHEMA = 'schema', +} export const ML_MANAGE_TRAINED_MODELS_PATH = '/app/ml/trained_models'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 11458565f781a6..dcb343a2beec45 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -290,14 +290,9 @@ describe('useEnterpriseSearchEngineNav', () => { name: 'Preview', }, { - href: `/app/enterprise_search/content/engines/${engineName}/indices`, - id: 'enterpriseSearchEngineIndices', - name: 'Indices', - }, - { - href: `/app/enterprise_search/content/engines/${engineName}/schema`, - id: 'enterpriseSearchEngineSchema', - name: 'Schema', + href: `/app/enterprise_search/content/engines/${engineName}/content`, + id: 'enterpriseSearchApplicationsContent', + name: 'Content', }, { href: `/app/enterprise_search/content/engines/${engineName}/connect`, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index f06e32b9e30c82..7fbc354ff725f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -204,23 +204,14 @@ export const useEnterpriseSearchEngineNav = (engineName?: string, isEmptyState?: }), }, { - id: 'enterpriseSearchEngineIndices', - name: i18n.translate('xpack.enterpriseSearch.nav.engine.indicesTitle', { - defaultMessage: 'Indices', + id: 'enterpriseSearchApplicationsContent', + name: i18n.translate('xpack.enterpriseSearch.nav.engine.contentTitle', { + defaultMessage: 'Content', }), ...generateNavLink({ shouldNotCreateHref: true, - to: `${enginePath}/${EngineViewTabs.INDICES}`, - }), - }, - { - id: 'enterpriseSearchEngineSchema', - name: i18n.translate('xpack.enterpriseSearch.nav.engine.schemaTitle', { - defaultMessage: 'Schema', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: `${enginePath}/${EngineViewTabs.SCHEMA}`, + shouldShowActiveForSubroutes: true, + to: `${enginePath}/${EngineViewTabs.CONTENT}`, }), }, { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f6610dba543b2a..f7cf6a952a7961 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12229,7 +12229,6 @@ "xpack.enterpriseSearch.content.engine.indices.docsCount.columnTitle": "Nombre de documents", "xpack.enterpriseSearch.content.engine.indices.health.columnTitle": "Intégrité des index", "xpack.enterpriseSearch.content.engine.indices.name.columnTitle": "Nom de l'index", - "xpack.enterpriseSearch.content.engine.indices.pageTitle": "Index", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description": "L'index ne sera pas supprimé. Vous pourrez l'ajouter de nouveau à ce moteur ultérieurement.", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text": "Oui, retirer cet index", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title": "Retirer cet index du moteur", @@ -12237,7 +12236,6 @@ "xpack.enterpriseSearch.content.engine.indicesSelect.docsLabel": "Documents :", "xpack.enterpriseSearch.content.engine.schema.field_name.columnTitle": "Nom du champ", "xpack.enterpriseSearch.content.engine.schema.field_type.columnTitle": "Type du champ", - "xpack.enterpriseSearch.content.engine.schema.pageTitle": "Schéma", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.confirmButton.title": "Oui, supprimer ce moteur ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.delete.description": "La suppression de votre moteur ne pourra pas être annulée. Vos index ne seront pas affectés. ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.title": "Supprimer définitivement ce moteur ?", @@ -13091,8 +13089,6 @@ "xpack.enterpriseSearch.nav.contentSettingsTitle": "Paramètres", "xpack.enterpriseSearch.nav.contentTitle": "Contenu", "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.engine.indicesTitle": "Index", - "xpack.enterpriseSearch.nav.engine.schemaTitle": "Schéma", "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "Aperçu", "xpack.enterpriseSearch.nav.searchExperiencesTitle": "Expériences de recherche", "xpack.enterpriseSearch.nav.searchIndicesTitle": "Index", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 91baf79cfaaa0a..2e5fdf22d200bb 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12228,7 +12228,6 @@ "xpack.enterpriseSearch.content.engine.indices.docsCount.columnTitle": "ドキュメント数", "xpack.enterpriseSearch.content.engine.indices.health.columnTitle": "インデックス正常性", "xpack.enterpriseSearch.content.engine.indices.name.columnTitle": "インデックス名", - "xpack.enterpriseSearch.content.engine.indices.pageTitle": "インデックス", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description": "インデックスは削除されません。後からこのエンジンに追加することができます。", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text": "はい。このインデックスを削除します", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title": "このインデックスをエンジンから削除", @@ -12236,7 +12235,6 @@ "xpack.enterpriseSearch.content.engine.indicesSelect.docsLabel": "ドキュメント:", "xpack.enterpriseSearch.content.engine.schema.field_name.columnTitle": "フィールド名", "xpack.enterpriseSearch.content.engine.schema.field_type.columnTitle": "フィールド型", - "xpack.enterpriseSearch.content.engine.schema.pageTitle": "スキーマ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.confirmButton.title": "はい。このエンジンを削除 ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.delete.description": "エンジンを削除すると、元に戻せません。インデックスには影響しません。", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.title": "このエンジンを完全に削除しますか?", @@ -13090,8 +13088,6 @@ "xpack.enterpriseSearch.nav.contentSettingsTitle": "設定", "xpack.enterpriseSearch.nav.contentTitle": "コンテンツ", "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.engine.indicesTitle": "インデックス", - "xpack.enterpriseSearch.nav.engine.schemaTitle": "スキーマ", "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "概要", "xpack.enterpriseSearch.nav.searchExperiencesTitle": "検索エクスペリエンス", "xpack.enterpriseSearch.nav.searchIndicesTitle": "インデックス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ed0f6a5c2fedb9..7199ea360f5e5b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12229,7 +12229,6 @@ "xpack.enterpriseSearch.content.engine.indices.docsCount.columnTitle": "文档计数", "xpack.enterpriseSearch.content.engine.indices.health.columnTitle": "索引运行状况", "xpack.enterpriseSearch.content.engine.indices.name.columnTitle": "索引名称", - "xpack.enterpriseSearch.content.engine.indices.pageTitle": "索引", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description": "这不会删除该索引。您可以在稍后将其重新添加到此引擎。", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text": "是,移除此索引", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title": "从引擎中移除此索引", @@ -12237,7 +12236,6 @@ "xpack.enterpriseSearch.content.engine.indicesSelect.docsLabel": "文档:", "xpack.enterpriseSearch.content.engine.schema.field_name.columnTitle": "字段名称", "xpack.enterpriseSearch.content.engine.schema.field_type.columnTitle": "字段类型", - "xpack.enterpriseSearch.content.engine.schema.pageTitle": "架构", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.confirmButton.title": "是,删除此引擎 ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.delete.description": "删除引擎是不可逆操作。您的索引不会受到影响。", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.title": "永久删除此引擎?", @@ -13091,8 +13089,6 @@ "xpack.enterpriseSearch.nav.contentSettingsTitle": "设置", "xpack.enterpriseSearch.nav.contentTitle": "内容", "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.engine.indicesTitle": "索引", - "xpack.enterpriseSearch.nav.engine.schemaTitle": "架构", "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "概览", "xpack.enterpriseSearch.nav.searchExperiencesTitle": "搜索体验", "xpack.enterpriseSearch.nav.searchIndicesTitle": "索引", From 32de23bdb348bea70b31c7746632d20b6193bc77 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 24 Apr 2023 12:40:48 -0700 Subject: [PATCH 62/65] [Dashboard] Scroll to new panel (#152056) ## Summary Closes #97064. This scrolls to a newly added panel on a dashboard instead of remaining at the top. The user can see the new panel without having to manually scroll to the bottom. ~This also scrolls to the maximized panel when you minimize instead of just throwing you back to the top of the dashboard.~ Note: Scrolling on minimize will be addressed in a future PR. This scrolling behavior also seems to work with portable dashboards embedded in another apps, but it may require additional work on the consumer to call `scrollToPanel` in the appropriate callbacks when adding panels. #### Scrolls to newly added panel and shows a success border animation ![Apr-18-2023 07-40-41](https://user-images.githubusercontent.com/1697105/232812491-5bf3ee3a-c81d-4dd3-8b04-67978da3b9a8.gif) #### Scrolls to panel on return from editor ![Apr-18-2023 07-56-35](https://user-images.githubusercontent.com/1697105/232817401-6cfd7085-91b6-4f05-be1c-e47f6cc3edab.gif) #### Scrolls to panel clone ![Apr-18-2023 07-54-43](https://user-images.githubusercontent.com/1697105/232816928-2b473778-76e1-4781-8e51-f9e46ab74b9b.gif) #### Scrolling in portable dashboards example ![Apr-18-2023 08-13-14](https://user-images.githubusercontent.com/1697105/232822632-ffcbd9ad-9cad-4185-931c-a68fbf7e0fbe.gif) ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Hannah Mudge --- .../controls_example/public/edit_example.tsx | 2 +- .../editor/open_add_data_control_flyout.tsx | 36 +++++++++++++----- .../dashboard_actions/clone_panel_action.tsx | 1 + .../dashboard_actions/expand_panel_action.tsx | 4 ++ .../replace_panel_flyout.tsx | 2 + .../add_data_control_button.tsx | 8 +++- .../add_time_slider_control_button.tsx | 7 +++- .../top_nav/dashboard_editing_toolbar.tsx | 2 + .../component/grid/_dashboard_grid.scss | 10 +++-- .../component/grid/dashboard_grid.tsx | 13 ++++++- .../component/grid/dashboard_grid_item.tsx | 16 +++++++- .../component/panel/_dashboard_panel.scss | 25 +++++++++++++ .../panel/dashboard_panel_placement.ts | 1 + .../embeddable/api/add_panel_from_library.ts | 4 ++ .../embeddable/api/panel_management.ts | 11 ++++-- .../embeddable/create/create_dashboard.ts | 22 +++++++---- .../embeddable/dashboard_container.tsx | 37 +++++++++++++++++++ .../state/dashboard_container_reducers.ts | 8 ++++ .../public/dashboard_container/types.ts | 2 + .../add_panel/add_panel_flyout.tsx | 6 ++- .../add_panel/open_add_panel_flyout.tsx | 3 ++ 21 files changed, 188 insertions(+), 32 deletions(-) diff --git a/examples/controls_example/public/edit_example.tsx b/examples/controls_example/public/edit_example.tsx index f6297befa615ce..148867337fedde 100644 --- a/examples/controls_example/public/edit_example.tsx +++ b/examples/controls_example/public/edit_example.tsx @@ -133,7 +133,7 @@ export const EditExample = () => { iconType="plusInCircle" isDisabled={controlGroupAPI === undefined} onClick={() => { - controlGroupAPI!.openAddDataControlFlyout(controlInputTransform); + controlGroupAPI!.openAddDataControlFlyout({ controlInputTransform }); }} > Add control diff --git a/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx b/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx index bf601934324887..695eaa42e064db 100644 --- a/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx +++ b/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; import { ControlGroupContainer, @@ -32,8 +33,12 @@ import { DataControlInput, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '.. export function openAddDataControlFlyout( this: ControlGroupContainer, - controlInputTransform?: ControlInputTransform + options?: { + controlInputTransform?: ControlInputTransform; + onSave?: (id: string) => void; + } ) { + const { controlInputTransform, onSave } = options || {}; const { overlays: { openFlyout, openConfirm }, controls: { getControlFactory }, @@ -71,7 +76,7 @@ export function openAddDataControlFlyout( updateTitle={(newTitle) => (controlInput.title = newTitle)} updateWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })} updateGrow={(defaultControlGrow: boolean) => this.updateInput({ defaultControlGrow })} - onSave={(type) => { + onSave={async (type) => { this.closeAllFlyouts(); if (!type) { return; @@ -86,17 +91,28 @@ export function openAddDataControlFlyout( controlInput = controlInputTransform({ ...controlInput }, type); } - if (type === OPTIONS_LIST_CONTROL) { - this.addOptionsListControl(controlInput as AddOptionsListControlProps); - return; - } + let newControl; - if (type === RANGE_SLIDER_CONTROL) { - this.addRangeSliderControl(controlInput as AddRangeSliderControlProps); - return; + switch (type) { + case OPTIONS_LIST_CONTROL: + newControl = await this.addOptionsListControl( + controlInput as AddOptionsListControlProps + ); + break; + case RANGE_SLIDER_CONTROL: + newControl = await this.addRangeSliderControl( + controlInput as AddRangeSliderControlProps + ); + break; + default: + newControl = await this.addDataControlFromField( + controlInput as AddDataControlProps + ); } - this.addDataControlFromField(controlInput as AddDataControlProps); + if (onSave && !isErrorEmbeddable(newControl)) { + onSave(newControl.id); + } }} onCancel={onCancel} onTypeEditorChange={(partialInput) => diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx index 482553e2f002fd..228db7138fd543 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx @@ -95,6 +95,7 @@ export class ClonePanelAction implements Action { height: panelToClone.gridData.h, currentPanels: dashboard.getInput().panels, placeBesideId: panelToClone.explicitInput.id, + scrollToPanel: true, } as IPanelPlacementBesideArgs ); } diff --git a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx index 0d3dd592dcc343..4e98a6dd310248 100644 --- a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx @@ -64,5 +64,9 @@ export class ExpandPanelAction implements Action { } const newValue = isExpanded(embeddable) ? undefined : embeddable.id; (embeddable.parent as DashboardContainer).setExpandedPanelId(newValue); + + if (!newValue) { + (embeddable.parent as DashboardContainer).setScrollToPanelId(embeddable.id); + } } } diff --git a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx index 4a2ac8f41d6a65..14067f0b6aa68f 100644 --- a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx @@ -21,6 +21,7 @@ import { Toast } from '@kbn/core/public'; import { DashboardPanelState } from '../../common'; import { pluginServices } from '../services/plugin_services'; import { dashboardReplacePanelActionStrings } from './_dashboard_actions_strings'; +import { DashboardContainer } from '../dashboard_container'; interface Props { container: IContainer; @@ -82,6 +83,7 @@ export class ReplacePanelFlyout extends React.Component { }, }); + (container as DashboardContainer).setHighlightPanelId(id); this.showToast(name); this.props.onClose(); }; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx index 6cef7e858b1655..e7c7daa2bcc274 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { ControlGroupContainer } from '@kbn/controls-plugin/public'; import { getAddControlButtonTitle } from '../../_dashboard_app_strings'; +import { useDashboardAPI } from '../../dashboard_app'; interface Props { closePopover: () => void; @@ -17,6 +18,11 @@ interface Props { } export const AddDataControlButton = ({ closePopover, controlGroup, ...rest }: Props) => { + const dashboard = useDashboardAPI(); + const onSave = () => { + dashboard.scrollToTop(); + }; + return ( { - controlGroup.openAddDataControlFlyout(); + controlGroup.openAddDataControlFlyout({ onSave }); closePopover(); }} > diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx index 8283144e1c1553..cbd514be8ba135 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx @@ -13,6 +13,7 @@ import { getAddTimeSliderControlButtonTitle, getOnlyOneTimeSliderControlMsg, } from '../../_dashboard_app_strings'; +import { useDashboardAPI } from '../../dashboard_app'; interface Props { closePopover: () => void; @@ -21,6 +22,7 @@ interface Props { export const AddTimeSliderControlButton = ({ closePopover, controlGroup, ...rest }: Props) => { const [hasTimeSliderControl, setHasTimeSliderControl] = useState(false); + const dashboard = useDashboardAPI(); useEffect(() => { const subscription = controlGroup.getInput$().subscribe(() => { @@ -42,8 +44,9 @@ export const AddTimeSliderControlButton = ({ closePopover, controlGroup, ...rest { - controlGroup.addTimeSliderControl(); + onClick={async () => { + await controlGroup.addTimeSliderControl(); + dashboard.scrollToTop(); closePopover(); }} data-test-subj="controls-create-timeslider-button" diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 03b609ae99736d..708af176d785d4 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -110,6 +110,8 @@ export function DashboardEditingToolbar() { const newEmbeddable = await dashboard.addNewEmbeddable(embeddableFactory.type, explicitInput); if (newEmbeddable) { + dashboard.setScrollToPanelId(newEmbeddable.id); + dashboard.setHighlightPanelId(newEmbeddable.id); toasts.addSuccess({ title: dashboardReplacePanelActionStrings.getSuccessMessage(newEmbeddable.getTitle()), 'data-test-subj': 'addEmbeddableToDashboardSuccess', diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss index 7e9529a90be8b5..cc96c816ce8b78 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss @@ -36,10 +36,13 @@ } /** - * When a single panel is expanded, all the other panels are hidden in the grid. + * When a single panel is expanded, all the other panels moved offscreen. + * Shifting the rendered panels offscreen prevents a quick flash when redrawing the panels on minimize */ .dshDashboardGrid__item--hidden { - display: none; + position: absolute; + top: -9999px; + left: -9999px; } /** @@ -53,11 +56,12 @@ * 1. We need to mark this as important because react grid layout sets the width and height of the panels inline. */ .dshDashboardGrid__item--expanded { + position: absolute; height: 100% !important; /* 1 */ width: 100% !important; /* 1 */ top: 0 !important; /* 1 */ left: 0 !important; /* 1 */ - transform: translate(0, 0) !important; /* 1 */ + transform: none !important; padding: $euiSizeS; // Altered panel styles can be found in ../panel diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx index b840fcd408977d..0055e24685b89d 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx @@ -12,7 +12,7 @@ import 'react-grid-layout/css/styles.css'; import { pick } from 'lodash'; import classNames from 'classnames'; import { useEffectOnce } from 'react-use/lib'; -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layout'; import { ViewMode } from '@kbn/embeddable-plugin/public'; @@ -38,6 +38,15 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { setTimeout(() => setAnimatePanelTransforms(true), 500); }); + useEffect(() => { + if (expandedPanelId) { + setAnimatePanelTransforms(false); + } else { + // delaying enabling CSS transforms to the next tick prevents a panel slide animation on minimize + setTimeout(() => setAnimatePanelTransforms(true), 0); + } + }, [expandedPanelId]); + const { onPanelStatusChange } = useDashboardPerformanceTracker({ panelCount: Object.keys(panels).length, }); @@ -98,7 +107,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { 'dshLayout-withoutMargins': !useMargins, 'dshLayout--viewing': viewMode === ViewMode.VIEW, 'dshLayout--editing': viewMode !== ViewMode.VIEW, - 'dshLayout--noAnimation': !animatePanelTransforms, + 'dshLayout--noAnimation': !animatePanelTransforms || expandedPanelId, 'dshLayout-isMaximizedPanel': expandedPanelId !== undefined, }); diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx index 45aa70fd50febc..39ff6ebc484184 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import classNames from 'classnames'; @@ -56,6 +56,8 @@ const Item = React.forwardRef( embeddable: { EmbeddablePanel: PanelComponent }, } = pluginServices.getServices(); const container = useDashboardContainer(); + const scrollToPanelId = container.select((state) => state.componentState.scrollToPanelId); + const highlightPanelId = container.select((state) => state.componentState.highlightPanelId); const expandPanel = expandedPanelId !== undefined && expandedPanelId === id; const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id; @@ -66,11 +68,23 @@ const Item = React.forwardRef( printViewport__vis: container.getInput().viewMode === ViewMode.PRINT, }); + useLayoutEffect(() => { + if (typeof ref !== 'function' && ref?.current) { + if (scrollToPanelId === id) { + container.scrollToPanel(ref.current); + } + if (highlightPanelId === id) { + container.highlightPanel(ref.current); + } + } + }, [id, container, scrollToPanelId, highlightPanelId, ref]); + return (
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss b/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss index f04e5e29d960b7..f8715220ddf378 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss @@ -11,6 +11,10 @@ box-shadow: none; border-radius: 0; } + + .dshDashboardGrid__item--highlighted { + border-radius: 0; + } } // Remove border color unless in editing mode @@ -25,3 +29,24 @@ cursor: default; } } + +@keyframes highlightOutline { + 0% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, 1); + } + 25% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, .5); + } + 100% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, 1); + } +} + +.dshDashboardGrid__item--highlighted { + border-radius: $euiSizeXS; + animation-name: highlightOutline; + animation-duration: 4s; + animation-timing-function: ease-out; + // keeps outline from getting cut off by other panels without margins + z-index: 999 !important; +} diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts index 77b51874319baa..e570e1eadd6ca7 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts @@ -24,6 +24,7 @@ export interface IPanelPlacementArgs { width: number; height: number; currentPanels: { [key: string]: DashboardPanelState }; + scrollToPanel?: boolean; } export interface IPanelPlacementBesideArgs extends IPanelPlacementArgs { diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts index ef4f4dc7ea5c9b..c708937e3d56e4 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts @@ -41,6 +41,10 @@ export function addFromLibrary(this: DashboardContainer) { notifications, overlays, theme, + onAddPanel: (id: string) => { + this.setScrollToPanelId(id); + this.setHighlightPanelId(id); + }, }) ); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts index cb2ce9af37bcd8..7b02001a93c6c4 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts @@ -128,7 +128,12 @@ export function showPlaceholderUntil newStateComplete) - .then((newPanelState: Partial) => - this.replacePanel(placeholderPanelState, newPanelState) - ); + .then(async (newPanelState: Partial) => { + const panelId = await this.replacePanel(placeholderPanelState, newPanelState); + + if (placementArgs?.scrollToPanel) { + this.setScrollToPanelId(panelId); + this.setHighlightPanelId(panelId); + } + }); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index 7609a4f3eb95fe..f0a20e832e431f 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -181,12 +181,13 @@ export const createDashboard = async ( const incomingEmbeddable = creationOptions?.incomingEmbeddable; if (incomingEmbeddable) { initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable. - if ( + + const panelExists = incomingEmbeddable.embeddableId && - Boolean(initialInput.panels[incomingEmbeddable.embeddableId]) - ) { + Boolean(initialInput.panels[incomingEmbeddable.embeddableId]); + if (panelExists) { // this embeddable already exists, we will update the explicit input. - const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId]; + const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId as string]; const sameType = panelToUpdate.type === incomingEmbeddable.type; panelToUpdate.type = incomingEmbeddable.type; @@ -195,17 +196,22 @@ export const createDashboard = async ( ...(sameType ? panelToUpdate.explicitInput : {}), ...incomingEmbeddable.input, - id: incomingEmbeddable.embeddableId, + id: incomingEmbeddable.embeddableId as string, // maintain hide panel titles setting. hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles, }; } else { // otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created. - untilDashboardReady().then((container) => - container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input) - ); + untilDashboardReady().then(async (container) => { + container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input); + }); } + + untilDashboardReady().then(async (container) => { + container.setScrollToPanelId(incomingEmbeddable.embeddableId); + container.setHighlightPanelId(incomingEmbeddable.embeddableId); + }); } // -------------------------------------------------------------------------------------- diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 5b7a589afa950b..d5a5385e779b36 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -398,4 +398,41 @@ export class DashboardContainer extends Container { + this.dispatch.setScrollToPanelId(id); + }; + + public scrollToPanel = async (panelRef: HTMLDivElement) => { + const id = this.getState().componentState.scrollToPanelId; + if (!id) return; + + this.untilEmbeddableLoaded(id).then(() => { + this.setScrollToPanelId(undefined); + panelRef.scrollIntoView({ block: 'center' }); + }); + }; + + public scrollToTop = () => { + window.scroll(0, 0); + }; + + public setHighlightPanelId = (id: string | undefined) => { + this.dispatch.setHighlightPanelId(id); + }; + + public highlightPanel = (panelRef: HTMLDivElement) => { + const id = this.getState().componentState.highlightPanelId; + + if (id && panelRef) { + this.untilEmbeddableLoaded(id).then(() => { + panelRef.classList.add('dshDashboardGrid__item--highlighted'); + // Removes the class after the highlight animation finishes + setTimeout(() => { + panelRef.classList.remove('dshDashboardGrid__item--highlighted'); + }, 5000); + }); + } + this.setHighlightPanelId(undefined); + }; } diff --git a/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts b/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts index 70bf3a7d659896..86a58bb72f639b 100644 --- a/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts +++ b/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts @@ -209,4 +209,12 @@ export const dashboardContainerReducers = { setHasOverlays: (state: DashboardReduxState, action: PayloadAction) => { state.componentState.hasOverlays = action.payload; }, + + setScrollToPanelId: (state: DashboardReduxState, action: PayloadAction) => { + state.componentState.scrollToPanelId = action.payload; + }, + + setHighlightPanelId: (state: DashboardReduxState, action: PayloadAction) => { + state.componentState.highlightPanelId = action.payload; + }, }; diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index 6e8ff1f5c98a05..544317d9f6bccf 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -33,6 +33,8 @@ export interface DashboardPublicState { fullScreenMode?: boolean; savedQueryId?: string; lastSavedId?: string; + scrollToPanelId?: string; + highlightPanelId?: string; } export interface DashboardRenderPerformanceStats { diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index dcaa3880678abb..ea7c150bf38b8a 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -30,6 +30,7 @@ interface Props { SavedObjectFinder: React.ComponentType; showCreateNewMenu?: boolean; reportUiCounter?: UsageCollectionStart['reportUiCounter']; + onAddPanel?: (id: string) => void; } interface State { @@ -101,7 +102,7 @@ export class AddPanelFlyout extends React.Component { throw new EmbeddableFactoryNotFoundError(savedObjectType); } - this.props.container.addNewEmbeddable( + const embeddable = await this.props.container.addNewEmbeddable( factoryForSavedObjectType.type, { savedObjectId } ); @@ -109,6 +110,9 @@ export class AddPanelFlyout extends React.Component { this.doTelemetryForAddEvent(this.props.container.type, factoryForSavedObjectType, so); this.showToast(name); + if (this.props.onAddPanel) { + this.props.onAddPanel(embeddable.id); + } }; private doTelemetryForAddEvent( diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index 4cc5a7ccb6e11f..eb2722dcf98690 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -24,6 +24,7 @@ export function openAddPanelFlyout(options: { showCreateNewMenu?: boolean; reportUiCounter?: UsageCollectionStart['reportUiCounter']; theme: ThemeServiceStart; + onAddPanel?: (id: string) => void; }): OverlayRef { const { embeddable, @@ -35,11 +36,13 @@ export function openAddPanelFlyout(options: { showCreateNewMenu, reportUiCounter, theme, + onAddPanel, } = options; const flyoutSession = overlays.openFlyout( toMountPoint( { if (flyoutSession) { flyoutSession.close(); From 275c36031428d7ec2881e0c57154d1b3a96ade5f Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 24 Apr 2023 16:19:59 -0400 Subject: [PATCH 63/65] [Synthetics] enable auto re-generation of monitor management api when read permissions are missing (#155203) Resolves https://github.com/elastic/kibana/issues/151695 Auto regenerates the synthetics api key when it does not include `synthetics-*` read permissions. Also ensures key are regenerated when deleted via stack management. A user without permissions to enable monitor management will see this callout when monitor management is disabled for either reason ![Synthetics-Overview-Synthetics-Kibana (1)](https://user-images.githubusercontent.com/11356435/232926046-ea39115b-acc7-40a7-8ec1-de77a20daf53.png) ## Testing lack of read permissions This PR is hard to test. I did so by adjusting the code to force the creation of an api key without read permissions. Here's how I did it: 1. connect to a clean ES instance by creating a new oblt cluster or running `yarn es snapshot 2. Remove read permissions for the api key https://github.com/elastic/kibana/pull/155203/files#diff-e38e55402aedfdb1a8a17bdd557364cd3649e1590b5e92fb44ed639f03ba880dR30 3. Remove read permission check here https://github.com/elastic/kibana/pull/155203/files#diff-e38e55402aedfdb1a8a17bdd557364cd3649e1590b5e92fb44ed639f03ba880dR60 4. Navigate to Synthetics app and create your first monitor 5. Navigate to Stack Management -> Api Keys. Click on he api key to inspect it's privileges. You should not see `read` permissions. 6. Remove the changes listed in step 2 and 3 and make sure the branch is back in sync with this PR 7. Navigate to the Synthetics app again. 9. Navigate to stack management -> api keys. Ensure there is only one synthetics monitor management api key. Click on he api key to inspect it's privileges. You should now see `read` permissions. 10. Delete this api key 11. Navigate back to the Synthetics app 12. Navigate back to stack management -> api keys. Notice tha api key has been regenerated --- .../management/disabled_callout.tsx | 40 +-- .../management/invalid_api_key_callout.tsx | 80 ----- .../monitors_page/management/labels.ts | 6 +- .../synthetics_enablement.tsx | 40 +-- .../monitors_page/overview/overview_page.tsx | 2 +- .../apps/synthetics/hooks/use_enablement.ts | 11 +- .../state/synthetics_enablement/actions.ts | 14 - .../state/synthetics_enablement/api.ts | 10 +- .../state/synthetics_enablement/effects.ts | 27 +- .../state/synthetics_enablement/index.ts | 39 -- .../lib/saved_objects/service_api_key.ts | 2 +- .../plugins/synthetics/server/routes/index.ts | 2 - .../routes/synthetics_service/enablement.ts | 56 +-- .../synthetics_service/get_api_key.test.ts | 63 +++- .../server/synthetics_service/get_api_key.ts | 26 +- .../translations/translations/fr-FR.json | 5 - .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - .../apis/synthetics/add_monitor_project.ts | 2 +- .../apis/synthetics/edit_monitor.ts | 2 +- .../apis/synthetics/get_monitor.ts | 2 +- .../apis/synthetics/get_monitor_overview.ts | 2 +- .../apis/synthetics/synthetics_enablement.ts | 334 ++++++++++++++---- 23 files changed, 395 insertions(+), 380 deletions(-) delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx index 0a75b9a44499bd..885f44eaa5ae1d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx @@ -6,41 +6,24 @@ */ import React from 'react'; -import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; -import { InvalidApiKeyCalloutCallout } from './invalid_api_key_callout'; +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import * as labels from './labels'; import { useEnablement } from '../../../hooks'; export const DisabledCallout = ({ total }: { total: number }) => { - const { enablement, enableSynthetics, invalidApiKeyError, loading } = useEnablement(); + const { enablement, invalidApiKeyError, loading } = useEnablement(); const showDisableCallout = !enablement.isEnabled && total > 0; - const showInvalidApiKeyError = invalidApiKeyError && total > 0; + const showInvalidApiKeyCallout = invalidApiKeyError && total > 0; - if (showInvalidApiKeyError) { - return ; - } - - if (!showDisableCallout) { + if (!showDisableCallout && !showInvalidApiKeyCallout) { return null; } - return ( - -

{labels.CALLOUT_MANAGEMENT_DESCRIPTION}

- {enablement.canEnable || loading ? ( - { - enableSynthetics(); - }} - isLoading={loading} - > - {labels.SYNTHETICS_ENABLE_LABEL} - - ) : ( + return !enablement.canEnable && !loading ? ( + <> + +

{labels.CALLOUT_MANAGEMENT_DESCRIPTION}

{labels.CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} { {labels.LEARN_MORE_LABEL}

- )} -
- ); +
+ + + ) : null; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx deleted file mode 100644 index 70816a69c2188b..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx +++ /dev/null @@ -1,80 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useEnablement } from '../../../hooks'; - -export const InvalidApiKeyCalloutCallout = () => { - const { enablement, enableSynthetics, loading } = useEnablement(); - - return ( - <> - -

{CALLOUT_MANAGEMENT_DESCRIPTION}

- {enablement.canEnable || loading ? ( - { - enableSynthetics(); - }} - isLoading={loading} - > - {SYNTHETICS_ENABLE_LABEL} - - ) : ( -

- {CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} - - {LEARN_MORE_LABEL} - -

- )} -
- - - ); -}; - -const LEARN_MORE_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey', - { - defaultMessage: 'Learn more', - } -); - -const API_KEY_MISSING = i18n.translate('xpack.synthetics.monitorManagement.callout.apiKeyMissing', { - defaultMessage: 'Monitor Management is currently disabled because of missing API key', -}); - -const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( - 'xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey', - { - defaultMessage: 'Contact your administrator to enable Monitor Management.', - } -); - -const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( - 'xpack.synthetics.monitorManagement.callout.description.invalidKey', - { - defaultMessage: `Monitor Management is currently disabled. To run your monitors in one of Elastic's global managed testing locations, you need to re-enable monitor management.`, - } -); - -const SYNTHETICS_ENABLE_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey', - { - defaultMessage: 'Enable monitor management', - } -); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts index aca280e74fcb23..ff297267dcb62d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts @@ -24,14 +24,14 @@ export const LEARN_MORE_LABEL = i18n.translate( export const CALLOUT_MANAGEMENT_DISABLED = i18n.translate( 'xpack.synthetics.monitorManagement.callout.disabled', { - defaultMessage: 'Monitor Management is disabled', + defaultMessage: 'Monitor Management is currently disabled', } ); export const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( 'xpack.synthetics.monitorManagement.callout.disabled.adminContact', { - defaultMessage: 'Please contact your administrator to enable Monitor Management.', + defaultMessage: 'Monitor Management will be enabled when an admin visits the Synthetics app.', } ); @@ -39,7 +39,7 @@ export const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( 'xpack.synthetics.monitorManagement.callout.description.disabled', { defaultMessage: - 'Monitor Management is currently disabled. To run your monitors on Elastic managed Synthetics service, enable Monitor Management. Your existing monitors are paused.', + "Monitor Management requires a valid API key to run your monitors on Elastic's global managed testing locations. If you already had enabled Monitor Management previously, the API key may no longer be valid.", } ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx index d6b927bbc43b3c..e4fcee12f65a51 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx @@ -6,16 +6,16 @@ */ import React, { useState, useEffect, useRef } from 'react'; -import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiTitle, EuiLink } from '@elastic/eui'; import { useEnablement } from '../../../../hooks/use_enablement'; import { kibanaService } from '../../../../../../utils/kibana_service'; import * as labels from './labels'; export const EnablementEmptyState = () => { - const { error, enablement, enableSynthetics, loading } = useEnablement(); + const { error, enablement, loading } = useEnablement(); const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); const [isEnabling, setIsEnabling] = useState(false); - const { isEnabled, canEnable } = enablement; + const { isEnabled } = enablement; const isEnabledRef = useRef(isEnabled); const buttonRef = useRef(null); @@ -44,11 +44,6 @@ export const EnablementEmptyState = () => { } }, [isEnabled, isEnabling, error]); - const handleEnableSynthetics = () => { - enableSynthetics(); - setIsEnabling(true); - }; - useEffect(() => { if (shouldFocusEnablementButton) { buttonRef.current?.focus(); @@ -57,33 +52,8 @@ export const EnablementEmptyState = () => { return !isEnabled && !loading ? ( - {canEnable - ? labels.MONITOR_MANAGEMENT_ENABLEMENT_LABEL - : labels.SYNTHETICS_APP_DISABLED_LABEL} -

- } - body={ -

- {canEnable - ? labels.MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE - : labels.MONITOR_MANAGEMENT_DISABLED_MESSAGE} -

- } - actions={ - canEnable ? ( - - {labels.MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL} - - ) : null - } + title={

{labels.SYNTHETICS_APP_DISABLED_LABEL}

} + body={

{labels.MONITOR_MANAGEMENT_DISABLED_MESSAGE}

} footer={ <> diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx index a98f78249adbe3..2e4eb6ca03a31c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx @@ -101,8 +101,8 @@ export const OverviewPage: React.FC = () => { return ( <> - + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts index 394da8aefc0868..fe726d0cbe3d2e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts @@ -5,14 +5,9 @@ * 2.0. */ -import { useEffect, useCallback } from 'react'; +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - getSyntheticsEnablement, - enableSynthetics, - disableSynthetics, - selectSyntheticsEnablement, -} from '../state'; +import { getSyntheticsEnablement, selectSyntheticsEnablement } from '../state'; export function useEnablement() { const dispatch = useDispatch(); @@ -35,7 +30,5 @@ export function useEnablement() { invalidApiKeyError: enablement ? !Boolean(enablement?.isValidApiKey) : false, error, loading, - enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]), - disableSynthetics: useCallback(() => dispatch(disableSynthetics()), [dispatch]), }; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts index 7369ce0917e5a9..78c0d9484149ed 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts @@ -16,17 +16,3 @@ export const getSyntheticsEnablementSuccess = createAction( '[SYNTHETICS_ENABLEMENT] GET FAILURE' ); - -export const disableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] DISABLE'); -export const disableSyntheticsSuccess = createAction<{}>('[SYNTHETICS_ENABLEMENT] DISABLE SUCCESS'); -export const disableSyntheticsFailure = createAction( - '[SYNTHETICS_ENABLEMENT] DISABLE FAILURE' -); - -export const enableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] ENABLE'); -export const enableSyntheticsSuccess = createAction( - '[SYNTHETICS_ENABLEMENT] ENABLE SUCCESS' -); -export const enableSyntheticsFailure = createAction( - '[SYNTHETICS_ENABLEMENT] ENABLE FAILURE' -); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts index 62b48676e39653..2e009cc0b89d21 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts @@ -14,17 +14,9 @@ import { apiService } from '../../../../utils/api_service'; export const fetchGetSyntheticsEnablement = async (): Promise => { - return await apiService.get( + return await apiService.put( API_URLS.SYNTHETICS_ENABLEMENT, undefined, MonitorManagementEnablementResultCodec ); }; - -export const fetchDisableSynthetics = async (): Promise<{}> => { - return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT); -}; - -export const fetchEnableSynthetics = async (): Promise => { - return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts index d3134c60f8fd37..14c912b07ce99a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts @@ -5,20 +5,15 @@ * 2.0. */ -import { takeLatest, takeLeading } from 'redux-saga/effects'; +import { takeLeading } from 'redux-saga/effects'; +import { i18n } from '@kbn/i18n'; import { getSyntheticsEnablement, getSyntheticsEnablementSuccess, getSyntheticsEnablementFailure, - disableSynthetics, - disableSyntheticsSuccess, - disableSyntheticsFailure, - enableSynthetics, - enableSyntheticsSuccess, - enableSyntheticsFailure, } from './actions'; -import { fetchGetSyntheticsEnablement, fetchDisableSynthetics, fetchEnableSynthetics } from './api'; import { fetchEffectFactory } from '../utils/fetch_effect'; +import { fetchGetSyntheticsEnablement } from './api'; export function* fetchSyntheticsEnablementEffect() { yield takeLeading( @@ -26,15 +21,13 @@ export function* fetchSyntheticsEnablementEffect() { fetchEffectFactory( fetchGetSyntheticsEnablement, getSyntheticsEnablementSuccess, - getSyntheticsEnablementFailure + getSyntheticsEnablementFailure, + undefined, + failureMessage ) ); - yield takeLatest( - disableSynthetics, - fetchEffectFactory(fetchDisableSynthetics, disableSyntheticsSuccess, disableSyntheticsFailure) - ); - yield takeLatest( - enableSynthetics, - fetchEffectFactory(fetchEnableSynthetics, enableSyntheticsSuccess, enableSyntheticsFailure) - ); } + +const failureMessage = i18n.translate('xpack.synthetics.settings.enablement.fail', { + defaultMessage: 'Failed to enable Monitor Management', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts index 62cbce9bfe05b5..26bf2b50b8325b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts @@ -9,12 +9,6 @@ import { createReducer } from '@reduxjs/toolkit'; import { getSyntheticsEnablement, getSyntheticsEnablementSuccess, - disableSynthetics, - disableSyntheticsSuccess, - disableSyntheticsFailure, - enableSynthetics, - enableSyntheticsSuccess, - enableSyntheticsFailure, getSyntheticsEnablementFailure, } from './actions'; import { MonitorManagementEnablementResult } from '../../../../../common/runtime_types'; @@ -45,39 +39,6 @@ export const syntheticsEnablementReducer = createReducer(initialState, (builder) .addCase(getSyntheticsEnablementFailure, (state, action) => { state.loading = false; state.error = action.payload; - }) - - .addCase(disableSynthetics, (state) => { - state.loading = true; - }) - .addCase(disableSyntheticsSuccess, (state, action) => { - state.loading = false; - state.error = null; - state.enablement = { - canEnable: state.enablement?.canEnable ?? false, - areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, - canManageApiKeys: state.enablement?.canManageApiKeys ?? false, - isEnabled: false, - isValidApiKey: true, - }; - }) - .addCase(disableSyntheticsFailure, (state, action) => { - state.loading = false; - state.error = action.payload; - }) - - .addCase(enableSynthetics, (state) => { - state.loading = true; - state.enablement = null; - }) - .addCase(enableSyntheticsSuccess, (state, action) => { - state.loading = false; - state.error = null; - state.enablement = action.payload; - }) - .addCase(enableSyntheticsFailure, (state, action) => { - state.loading = false; - state.error = action.payload; }); }); diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts index adab53c9d42683..3c62f99f7e67be 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts @@ -72,7 +72,7 @@ const getSyntheticsServiceAPIKey = async (server: UptimeServerSetup) => { } }; -const setSyntheticsServiceApiKey = async ( +export const setSyntheticsServiceApiKey = async ( soClient: SavedObjectsClientContract, apiKey: SyntheticsServiceApiKey ) => { diff --git a/x-pack/plugins/synthetics/server/routes/index.ts b/x-pack/plugins/synthetics/server/routes/index.ts index 9e2038d05962a0..836143d55f014e 100644 --- a/x-pack/plugins/synthetics/server/routes/index.ts +++ b/x-pack/plugins/synthetics/server/routes/index.ts @@ -20,7 +20,6 @@ import { getServiceLocationsRoute } from './synthetics_service/get_service_locat import { deleteSyntheticsMonitorRoute } from './monitor_cruds/delete_monitor'; import { disableSyntheticsRoute, - enableSyntheticsRoute, getSyntheticsEnablementRoute, } from './synthetics_service/enablement'; import { @@ -61,7 +60,6 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ deleteSyntheticsMonitorProjectRoute, disableSyntheticsRoute, editSyntheticsMonitorRoute, - enableSyntheticsRoute, getServiceLocationsRoute, getSyntheticsMonitorRoute, getSyntheticsProjectMonitorsRoute, diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts index c4561f3ee9e00b..87a10dbee9a8eb 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts +++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts @@ -5,18 +5,12 @@ * 2.0. */ import { syntheticsServiceAPIKeySavedObject } from '../../legacy_uptime/lib/saved_objects/service_api_key'; -import { - SyntheticsRestApiRouteFactory, - UMRestApiRouteFactory, -} from '../../legacy_uptime/routes/types'; +import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; -import { - generateAndSaveServiceAPIKey, - SyntheticsForbiddenError, -} from '../../synthetics_service/get_api_key'; +import { generateAndSaveServiceAPIKey } from '../../synthetics_service/get_api_key'; -export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({ - method: 'GET', +export const getSyntheticsEnablementRoute: SyntheticsRestApiRouteFactory = (libs) => ({ + method: 'PUT', path: API_URLS.SYNTHETICS_ENABLEMENT, validate: {}, handler: async ({ savedObjectsClient, request, server }): Promise => { @@ -25,7 +19,18 @@ export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({ server, }); const { canEnable, isEnabled } = result; - if (canEnable && !isEnabled && server.config.service?.manifestUrl) { + const { security } = server; + const { apiKey, isValid } = await libs.requests.getAPIKeyForSyntheticsService({ + server, + }); + if (apiKey && !isValid) { + await syntheticsServiceAPIKeySavedObject.delete(savedObjectsClient); + await security.authc.apiKeys?.invalidateAsInternalUser({ + ids: [apiKey?.id || ''], + }); + } + const regenerationRequired = !isEnabled || !isValid; + if (canEnable && regenerationRequired && server.config.service?.manifestUrl) { await generateAndSaveServiceAPIKey({ request, authSavedObjectsClient: savedObjectsClient, @@ -68,7 +73,7 @@ export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ( server, }); await syntheticsServiceAPIKeySavedObject.delete(savedObjectsClient); - await security.authc.apiKeys?.invalidate(request, { ids: [apiKey?.id || ''] }); + await security.authc.apiKeys?.invalidateAsInternalUser({ ids: [apiKey?.id || ''] }); return response.ok({}); } catch (e) { server.logger.error(e); @@ -76,30 +81,3 @@ export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ( } }, }); - -export const enableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({ - method: 'POST', - path: API_URLS.SYNTHETICS_ENABLEMENT, - validate: {}, - handler: async ({ request, response, server, savedObjectsClient }): Promise => { - const { logger } = server; - try { - await generateAndSaveServiceAPIKey({ - request, - authSavedObjectsClient: savedObjectsClient, - server, - }); - return response.ok({ - body: await libs.requests.getSyntheticsEnablement({ - server, - }), - }); - } catch (e) { - logger.error(e); - if (e instanceof SyntheticsForbiddenError) { - return response.forbidden(); - } - throw e; - } - }, -}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts index 4b15f4da43515d..ca4a18e88d5d95 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts @@ -25,16 +25,6 @@ describe('getAPIKeyTest', function () { const logger = loggerMock.create(); - jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ - index: { - [syntheticsIndex]: { - auto_configure: true, - create_doc: true, - view_index_metadata: true, - }, - }, - } as any); - const server = { logger, security, @@ -52,6 +42,20 @@ describe('getAPIKeyTest', function () { encoded: '@#$%^&', }); + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ + index: { + [syntheticsIndex]: { + auto_configure: true, + create_doc: true, + view_index_metadata: true, + read: true, + }, + }, + } as any); + }); + it('should return existing api key', async () => { const getObject = jest .fn() @@ -79,4 +83,43 @@ describe('getAPIKeyTest', function () { 'ba997842-b0cf-4429-aa9d-578d9bf0d391' ); }); + + it('invalidates api keys with missing read permissions', async () => { + jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ + index: { + [syntheticsIndex]: { + auto_configure: true, + create_doc: true, + view_index_metadata: true, + read: false, + }, + }, + } as any); + + const getObject = jest + .fn() + .mockReturnValue({ attributes: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' } }); + + encryptedSavedObjects.getClient = jest.fn().mockReturnValue({ + getDecryptedAsInternalUser: getObject, + }); + const apiKey = await getAPIKeyForSyntheticsService({ + server, + }); + + expect(apiKey).toEqual({ + apiKey: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' }, + isValid: false, + }); + + expect(encryptedSavedObjects.getClient).toHaveBeenCalledTimes(1); + expect(getObject).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjects.getClient).toHaveBeenCalledWith({ + includedHiddenTypes: [syntheticsServiceApiKey.name], + }); + expect(getObject).toHaveBeenCalledWith( + 'uptime-synthetics-api-key', + 'ba997842-b0cf-4429-aa9d-578d9bf0d391' + ); + }); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts index 0bc4f656901f45..79af4d6cfc7187 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts @@ -56,7 +56,8 @@ export const getAPIKeyForSyntheticsService = async ({ const hasPermissions = indexPermissions.auto_configure && indexPermissions.create_doc && - indexPermissions.view_index_metadata; + indexPermissions.view_index_metadata && + indexPermissions.read; if (!hasPermissions) { return { isValid: false, apiKey }; @@ -92,6 +93,7 @@ export const generateAPIKey = async ({ } if (uptimePrivileges) { + /* Exposed to the user. Must create directly with the user */ return security.authc.apiKeys?.create(request, { name: 'synthetics-api-key (required for project monitors)', kibana_role_descriptors: { @@ -122,7 +124,8 @@ export const generateAPIKey = async ({ throw new SyntheticsForbiddenError(); } - return security.authc.apiKeys?.create(request, { + /* Not exposed to the user. May grant as internal user */ + return security.authc.apiKeys?.grantAsInternalUser(request, { name: 'synthetics-api-key (required for monitor management)', role_descriptors: { synthetics_writer: serviceApiKeyPrivileges, @@ -160,23 +163,24 @@ export const generateAndSaveServiceAPIKey = async ({ export const getSyntheticsEnablement = async ({ server }: { server: UptimeServerSetup }) => { const { security, config } = server; + const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ + getAPIKeyForSyntheticsService({ server }), + hasEnablePermissions(server), + security.authc.apiKeys.areAPIKeysEnabled(), + ]); + + const { canEnable, canManageApiKeys } = hasPrivileges; + if (!config.service?.manifestUrl) { return { canEnable: true, - canManageApiKeys: true, + canManageApiKeys, isEnabled: true, isValidApiKey: true, areApiKeysEnabled: true, }; } - const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ - getAPIKeyForSyntheticsService({ server }), - hasEnablePermissions(server), - security.authc.apiKeys.areAPIKeysEnabled(), - ]); - - const { canEnable, canManageApiKeys } = hasPrivileges; return { canEnable, canManageApiKeys, @@ -217,7 +221,7 @@ const hasEnablePermissions = async ({ uptimeEsClient }: UptimeServerSetup) => { return { canManageApiKeys, - canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions, + canEnable: hasClusterPermissions && hasIndexPermissions, }; }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f7cf6a952a7961..fda996fca0a651 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -34922,12 +34922,9 @@ "xpack.synthetics.monitorManagement.apiKey.label": "Clé d'API", "xpack.synthetics.monitorManagement.apiKeyWarning.label": "Cette clé d’API ne sera affichée qu'une seule fois. Veuillez en conserver une copie pour vos propres dossiers.", "xpack.synthetics.monitorManagement.areYouSure": "Voulez-vous vraiment supprimer cet emplacement ?", - "xpack.synthetics.monitorManagement.callout.apiKeyMissing": "La Gestion des moniteurs est actuellement désactivée en raison d'une clé d'API manquante", "xpack.synthetics.monitorManagement.callout.description.disabled": "La Gestion des moniteurs est actuellement désactivée. Pour exécuter vos moniteurs sur le service Synthetics géré par Elastic, activez la Gestion des moniteurs. Vos moniteurs existants ont été suspendus.", - "xpack.synthetics.monitorManagement.callout.description.invalidKey": "La Gestion des moniteurs est actuellement désactivée. Pour exécuter vos moniteurs dans l'un des emplacements de tests gérés globaux d'Elastic, vous devez ré-activer la Gestion des moniteurs.", "xpack.synthetics.monitorManagement.callout.disabled": "La Gestion des moniteurs est désactivée", "xpack.synthetics.monitorManagement.callout.disabled.adminContact": "Veuillez contacter votre administrateur pour activer la Gestion des moniteurs.", - "xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey": "Contactez votre administrateur pour activer la Gestion des moniteurs.", "xpack.synthetics.monitorManagement.cancelLabel": "Annuler", "xpack.synthetics.monitorManagement.cannotSaveIntegration": "Vous n'êtes pas autorisé à mettre à jour les intégrations. Des autorisations d'écriture pour les intégrations sont requises.", "xpack.synthetics.monitorManagement.closeButtonLabel": "Fermer", @@ -34976,7 +34973,6 @@ "xpack.synthetics.monitorManagement.locationName": "Nom de l’emplacement", "xpack.synthetics.monitorManagement.locationsLabel": "Emplacements", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel": "Chargement de la liste Gestion des moniteurs", - "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey": "En savoir plus", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.learnMore": "En savoir plus.", "xpack.synthetics.monitorManagement.monitorAddedSuccessMessage": "Moniteur ajouté avec succès.", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "Moniteur mis à jour.", @@ -35017,7 +35013,6 @@ "xpack.synthetics.monitorManagement.steps": "Étapes", "xpack.synthetics.monitorManagement.summary.heading": "Résumé", "xpack.synthetics.monitorManagement.syntheticsDisabledSuccess": "Gestion des moniteurs désactivée avec succès.", - "xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey": "Activer la Gestion des moniteurs", "xpack.synthetics.monitorManagement.syntheticsEnableLabel.management": "Activer la Gestion des moniteurs", "xpack.synthetics.monitorManagement.syntheticsEnableSuccess": "Gestion des moniteurs activée avec succès.", "xpack.synthetics.monitorManagement.testResult": "Résultat du test", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2e5fdf22d200bb..11ccd44a6eee76 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34901,12 +34901,9 @@ "xpack.synthetics.monitorManagement.apiKey.label": "API キー", "xpack.synthetics.monitorManagement.apiKeyWarning.label": "このAPIキーは1回だけ表示されます。自分の記録用にコピーして保管してください。", "xpack.synthetics.monitorManagement.areYouSure": "この場所を削除しますか?", - "xpack.synthetics.monitorManagement.callout.apiKeyMissing": "現在、APIキーがないため、モニター管理は無効です", "xpack.synthetics.monitorManagement.callout.description.disabled": "モニター管理は現在無効です。Elasticで管理されたSyntheticsサービスでモニターを実行するには、モニター管理を有効にします。既存のモニターが一時停止しています。", - "xpack.synthetics.monitorManagement.callout.description.invalidKey": "モニター管理は現在無効です。Elasticのグローバル管理されたテストロケーションのいずれかでモニターを実行するには、モニター管理を再有効化する必要があります。", "xpack.synthetics.monitorManagement.callout.disabled": "モニター管理が無効です", "xpack.synthetics.monitorManagement.callout.disabled.adminContact": "モニター管理を有効にするには、管理者に連絡してください。", - "xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey": "モニター管理を有効にするには、管理者に連絡してください。", "xpack.synthetics.monitorManagement.cancelLabel": "キャンセル", "xpack.synthetics.monitorManagement.cannotSaveIntegration": "統合を更新する権限がありません。統合書き込み権限が必要です。", "xpack.synthetics.monitorManagement.closeButtonLabel": "閉じる", @@ -34955,7 +34952,6 @@ "xpack.synthetics.monitorManagement.locationName": "場所名", "xpack.synthetics.monitorManagement.locationsLabel": "場所", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel": "モニター管理を読み込んでいます", - "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey": "詳細", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.learnMore": "詳細情報", "xpack.synthetics.monitorManagement.monitorAddedSuccessMessage": "モニターが正常に追加されました。", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "モニターは正常に更新されました。", @@ -34996,7 +34992,6 @@ "xpack.synthetics.monitorManagement.steps": "ステップ", "xpack.synthetics.monitorManagement.summary.heading": "まとめ", "xpack.synthetics.monitorManagement.syntheticsDisabledSuccess": "モニター管理は正常に無効にされました。", - "xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey": "モニター管理を有効にする", "xpack.synthetics.monitorManagement.syntheticsEnableLabel.management": "モニター管理を有効にする", "xpack.synthetics.monitorManagement.syntheticsEnableSuccess": "モニター管理は正常に有効にされました。", "xpack.synthetics.monitorManagement.testResult": "テスト結果", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7199ea360f5e5b..4eae043b7b8846 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -34917,12 +34917,9 @@ "xpack.synthetics.monitorManagement.apiKey.label": "API 密钥", "xpack.synthetics.monitorManagement.apiKeyWarning.label": "此 API 密钥仅显示一次。请保留副本作为您自己的记录。", "xpack.synthetics.monitorManagement.areYouSure": "是否确定要删除此位置?", - "xpack.synthetics.monitorManagement.callout.apiKeyMissing": "由于缺少 API 密钥,监测管理当前已禁用", "xpack.synthetics.monitorManagement.callout.description.disabled": "监测管理当前处于禁用状态。要在 Elastic 托管 Synthetics 服务上运行监测,请启用监测管理。现有监测已暂停。", - "xpack.synthetics.monitorManagement.callout.description.invalidKey": "监测管理当前处于禁用状态。要在 Elastic 的全球托管测试位置之一运行监测,您需要重新启用监测管理。", "xpack.synthetics.monitorManagement.callout.disabled": "已禁用监测管理", "xpack.synthetics.monitorManagement.callout.disabled.adminContact": "请联系管理员启用监测管理。", - "xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey": "请联系管理员启用监测管理。", "xpack.synthetics.monitorManagement.cancelLabel": "取消", "xpack.synthetics.monitorManagement.cannotSaveIntegration": "您无权更新集成。需要集成写入权限。", "xpack.synthetics.monitorManagement.closeButtonLabel": "关闭", @@ -34971,7 +34968,6 @@ "xpack.synthetics.monitorManagement.locationName": "位置名称", "xpack.synthetics.monitorManagement.locationsLabel": "位置", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel": "正在加载监测管理", - "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey": "了解详情", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.learnMore": "了解详情。", "xpack.synthetics.monitorManagement.monitorAddedSuccessMessage": "已成功添加监测。", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "已成功更新监测。", @@ -35012,7 +35008,6 @@ "xpack.synthetics.monitorManagement.steps": "步长", "xpack.synthetics.monitorManagement.summary.heading": "摘要", "xpack.synthetics.monitorManagement.syntheticsDisabledSuccess": "已成功禁用监测管理。", - "xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey": "启用监测管理", "xpack.synthetics.monitorManagement.syntheticsEnableLabel.management": "启用监测管理", "xpack.synthetics.monitorManagement.syntheticsEnableSuccess": "已成功启用监测管理。", "xpack.synthetics.monitorManagement.testResult": "测试结果", diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index b1bc58abe71138..e6a8ae14cda1ae 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); await supertest .post('/api/fleet/epm/packages/synthetics/0.11.4') diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts index f20a2cdf61a45c..bf4447a1b59699 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts @@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { _httpMonitorJson = getFixtureJson('http_monitor'); await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); const testPolicyName = 'Fleet test server policy' + Date.now(); const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor.ts index 5394ca64545e69..00772c5550ac17 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor.ts @@ -32,7 +32,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); _monitors = [ getFixtureJson('icmp_monitor'), diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts index 25aad0704cddd0..625dbdac61608f 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts @@ -55,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); await security.role.create(roleName, { kibana: [ diff --git a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts index d031a6c505c8fa..bf68da4c148f5b 100644 --- a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts +++ b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts @@ -6,24 +6,57 @@ */ import { API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { + syntheticsApiKeyID, + syntheticsApiKeyObjectType, +} from '@kbn/synthetics-plugin/server/legacy_uptime/lib/saved_objects/service_api_key'; import { serviceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { + const correctPrivileges = { + applications: [], + cluster: ['monitor', 'read_ilm', 'read_pipeline'], + indices: [ + { + allow_restricted_indices: false, + names: ['synthetics-*'], + privileges: ['view_index_metadata', 'create_doc', 'auto_configure', 'read'], + }, + ], + metadata: {}, + run_as: [], + transient_metadata: { + enabled: true, + }, + }; + describe('SyntheticsEnablement', () => { const supertestWithAuth = getService('supertest'); const supertest = getService('supertestWithoutAuth'); const security = getService('security'); const kibanaServer = getService('kibanaServer'); - before(async () => { - await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); - }); - - describe('[GET] - /internal/uptime/service/enablement', () => { - ['manage_security', 'manage_own_api_key', 'manage_api_key'].forEach((privilege) => { - it(`returns response for an admin with privilege ${privilege}`, async () => { + const esSupertest = getService('esSupertest'); + + const getApiKeys = async () => { + const { body } = await esSupertest.get(`/_security/api_key`).query({ with_limited_by: true }); + const apiKeys = body.api_keys || []; + return apiKeys.filter( + (apiKey: any) => apiKey.name.includes('synthetics-api-key') && apiKey.invalidated === false + ); + }; + + describe('[PUT] /internal/uptime/service/enablement', () => { + beforeEach(async () => { + const apiKeys = await getApiKeys(); + if (apiKeys.length) { + await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); + } + }); + ['manage_security', 'manage_api_key', 'manage_own_api_key'].forEach((privilege) => { + it(`returns response when user can manage api keys`, async () => { const username = 'admin'; const roleName = `synthetics_admin-${privilege}`; const password = `${username}-password`; @@ -38,7 +71,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: [privilege, ...serviceApiKeyPrivileges.cluster], + cluster: [privilege], indices: serviceApiKeyPrivileges.indices, }, }); @@ -50,7 +83,7 @@ export default function ({ getService }: FtrProviderContext) { }); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -58,17 +91,10 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: true, - canEnable: true, - isEnabled: true, - isValidApiKey: true, + canEnable: false, + isEnabled: false, + isValidApiKey: false, }); - if (privilege !== 'manage_own_api_key') { - await supertest - .delete(API_URLS.SYNTHETICS_ENABLEMENT) - .auth(username, password) - .set('kbn-xsrf', 'true') - .expect(200); - } } finally { await security.user.delete(username); await security.role.delete(roleName); @@ -76,9 +102,9 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('returns response for an uptime all user without admin privileges', async () => { - const username = 'uptime'; - const roleName = 'uptime_user'; + it(`returns response for an admin with privilege`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; const password = `${username}-password`; try { await security.role.create(roleName, { @@ -90,7 +116,10 @@ export default function ({ getService }: FtrProviderContext) { spaces: ['*'], }, ], - elasticsearch: {}, + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, }); await security.user.create(username, { @@ -100,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) { }); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -108,19 +137,20 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: false, - canEnable: false, - isEnabled: false, - isValidApiKey: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, }); + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); } finally { - await security.role.delete(roleName); await security.user.delete(username); + await security.role.delete(roleName); } }); - }); - describe('[POST] - /internal/uptime/service/enablement', () => { - it('with an admin', async () => { + it(`does not create excess api keys`, async () => { const username = 'admin'; const roleName = `synthetics_admin`; const password = `${username}-password`; @@ -135,7 +165,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -146,38 +176,213 @@ export default function ({ getService }: FtrProviderContext) { full_name: 'a kibana user', }); - await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + + // call api a second time + const apiResponse2 = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + + expect(apiResponse2.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + } finally { + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it(`auto re-enables the api key when created with invalid permissions and invalidates old api key`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + try { + // create api key with incorrect permissions + const apiKeyResult = await esSupertest + .post(`/_security/api_key`) + .send({ + name: 'synthetics-api-key', + expiration: '1d', + role_descriptors: { + 'role-a': { + cluster: serviceApiKeyPrivileges.cluster, + indices: [ + { + names: ['synthetics-*'], + privileges: ['view_index_metadata', 'create_doc', 'auto_configure'], + }, + ], + }, + }, + }) + .expect(200); + kibanaServer.savedObjects.create({ + id: syntheticsApiKeyID, + type: syntheticsApiKeyObjectType, + overwrite: true, + attributes: { + id: apiKeyResult.body.id, + name: 'synthetics-api-key (required for monitor management)', + apiKey: apiKeyResult.body.api_key, + }, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).not.eql(correctPrivileges); + + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); } finally { - await supertest - .delete(API_URLS.SYNTHETICS_ENABLEMENT) + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it(`auto re-enables api key when invalidated`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + + // delete api key + await esSupertest + .delete(`/_security/api_key`) + .send({ + ids: [validApiKeys[0].id], + }) + .expect(200); + + const validApiKeysAferDeletion = await getApiKeys(); + expect(validApiKeysAferDeletion.length).eql(0); + + // call api a second time + const apiResponse2 = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + + expect(apiResponse2.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + } finally { await security.user.delete(username); await security.role.delete(roleName); } }); - it('with an uptime user', async () => { + it('returns response for an uptime all user without admin privileges', async () => { const username = 'uptime'; - const roleName = `uptime_user`; + const roleName = 'uptime_user'; const password = `${username}-password`; try { await security.role.create(roleName, { @@ -198,16 +403,12 @@ export default function ({ getService }: FtrProviderContext) { full_name: 'a kibana user', }); - await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) - .auth(username, password) - .set('kbn-xsrf', 'true') - .expect(403); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: false, @@ -216,13 +417,19 @@ export default function ({ getService }: FtrProviderContext) { isValidApiKey: false, }); } finally { - await security.user.delete(username); await security.role.delete(roleName); + await security.user.delete(username); } }); }); - describe('[DELETE] - /internal/uptime/service/enablement', () => { + describe('[DELETE] /internal/uptime/service/enablement', () => { + beforeEach(async () => { + const apiKeys = await getApiKeys(); + if (apiKeys.length) { + await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); + } + }); it('with an admin', async () => { const username = 'admin'; const roleName = `synthetics_admin`; @@ -238,7 +445,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -250,7 +457,7 @@ export default function ({ getService }: FtrProviderContext) { }); await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -261,14 +468,14 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(delResponse.body).eql({}); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -303,7 +510,7 @@ export default function ({ getService }: FtrProviderContext) { }); await supertestWithAuth - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .set('kbn-xsrf', 'true') .expect(200); await supertest @@ -312,7 +519,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(403); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -351,7 +558,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -364,21 +571,21 @@ export default function ({ getService }: FtrProviderContext) { // can enable synthetics in default space when enabled in a non default space const apiResponseGet = await supertest - .get(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponseGet.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, }); await supertest - .post(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -388,14 +595,14 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -403,7 +610,7 @@ export default function ({ getService }: FtrProviderContext) { // can disable synthetics in non default space when enabled in default space await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -413,14 +620,14 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); const apiResponse2 = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse2.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -428,6 +635,7 @@ export default function ({ getService }: FtrProviderContext) { } finally { await security.user.delete(username); await security.role.delete(roleName); + await kibanaServer.spaces.delete(SPACE_ID); } }); }); From b9551e2cf0281509781b4bc0b71a845d03e8600c Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Mon, 24 Apr 2023 23:05:00 +0100 Subject: [PATCH 64/65] Sec Telemetry: Add Kubernetes and misc fields to filterlist (#152129) ## Summary Adds Kubernetes and other fields to the telemetry allowlist. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Terrance DeJesus <99630311+terrancedejesus@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Isai <59296946+imays11@users.noreply.github.com> --- .../lib/telemetry/filterlists/index.test.ts | 21 +++++ .../filterlists/prebuilt_rules_alerts.ts | 87 +++++++++++++++++ .../server/lib/telemetry/helpers.test.ts | 94 +++++++++++++++++++ .../server/lib/telemetry/helpers.ts | 47 +++++++++- .../telemetry/tasks/prebuilt_rule_alerts.ts | 9 +- .../server/lib/telemetry/types.ts | 13 +++ 6 files changed, 268 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts index 9e5c383e825d17..631b67cd496014 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts @@ -26,6 +26,9 @@ describe('Security Telemetry filters', () => { 'event.provider': true, 'event.type': true, 'powershell.file.script_block_text': true, + 'kubernetes.pod.uid': true, + 'kubernetes.pod.name': true, + 'kubernetes.pod.ip': true, package_version: true, }; @@ -177,5 +180,23 @@ describe('Security Telemetry filters', () => { package_version: '3.4.1', }); }); + + it('copies over kubernetes fields', () => { + const event = { + not_event: 'much data, much wow', + 'event.id': '36857486973080746231799376445175633955031786243637182487', + 'event.ingested': 'May 17, 2022 @ 00:22:07.000', + 'kubernetes.pod.uid': '059a3767-7492-4fb5-92d4-93f458ddab44', + 'kubernetes.pod.name': 'kube-dns-6f4fd4zzz-7z7xj', + 'kubernetes.pod.ip': '10-245-0-5', + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + 'event.id': '36857486973080746231799376445175633955031786243637182487', + 'event.ingested': 'May 17, 2022 @ 00:22:07.000', + 'kubernetes.pod.uid': '059a3767-7492-4fb5-92d4-93f458ddab44', + 'kubernetes.pod.name': 'kube-dns-6f4fd4zzz-7z7xj', + 'kubernetes.pod.ip': '10-245-0-5', + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts index 225206cca4b0da..42235cae665749 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts @@ -215,6 +215,9 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { target_resources: true, }, }, + properties: { + category: true, + }, signinlogs: { properties: { app_display_name: true, @@ -253,6 +256,85 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { setting: { name: true, }, + application: { + name: true, + }, + old_value: true, + role: { + name: true, + }, + }, + event: { + type: true, + }, + }, + // kubernetes + kubernetes: { + audit: { + annotations: true, + verb: true, + user: { + groups: true, + }, + impersonatedUser: { + groups: true, + }, + objectRef: { + name: true, + namespace: true, + resource: true, + subresource: true, + }, + requestObject: { + spec: { + containers: { + image: true, + securityContext: { + allowPrivilegeEscalation: true, + capabilities: { + add: true, + }, + privileged: true, + procMount: true, + runAsGroup: true, + runAsUser: true, + }, + }, + hostIPC: true, + hostNetwork: true, + hostPID: true, + securityContext: { + runAsGroup: true, + runAsUser: true, + }, + serviceAccountName: true, + type: true, + volumes: { + hostPath: { + path: true, + }, + }, + }, + }, + requestURI: true, + responseObject: { + roleRef: { + kind: true, + resourceName: true, + }, + rules: true, + spec: { + containers: { + securityContext: { + allowPrivilegeEscalation: true, + }, + }, + }, + }, + responseStatus: { + code: true, + }, + userAgent: true, }, }, // office 360 @@ -275,6 +357,11 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { Enabled: true, ForwardAsAttachmentTo: true, ForwardTo: true, + ModifiedProperties: { + Role_DisplayName: { + NewValue: true, + }, + }, RedirectTo: true, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts index 4e3db8c657e047..e01eb21cbe68cc 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts @@ -25,6 +25,7 @@ import { tlog, setIsElasticCloudDeployment, createTaskMetric, + processK8sUsernames, } from './helpers'; import type { ESClusterInfo, ESLicense, ExceptionListItem } from './types'; import type { PolicyConfig, PolicyData } from '../../../common/endpoint/types'; @@ -963,6 +964,7 @@ describe.skip('test create task metrics', () => { passed: true, }); }); + test('can succeed when error given', async () => { const stubTaskName = 'test'; const stubPassed = false; @@ -982,3 +984,95 @@ describe.skip('test create task metrics', () => { }); }); }); + +describe('Pii is removed from a kubernetes prebuilt rule alert', () => { + test('a document without the sensitive values is ignored', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + audit: {}, + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + }, + powershell: { + command_line: 'test', + module: 'test', + module_loaded: 'test', + module_version: 'test', + process_name: 'test', + }, + }; + + const ignoredDocument = processK8sUsernames(clusterUuid, testDocument); + expect(ignoredDocument).toEqual(testDocument); + }); + + test('kubernetes system usernames are not sanitized from a document', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + audit: { + user: { + username: 'system:serviceaccount:default:default', + groups: [ + 'system:serviceaccounts', + 'system:serviceaccounts:default', + 'system:authenticated', + ], + }, + impersonated_user: { + username: 'system:serviceaccount:default:default', + groups: [ + 'system:serviceaccounts', + 'system:serviceaccounts:default', + 'system:authenticated', + ], + }, + }, + }, + }; + + const sanitizedDocument = processK8sUsernames(clusterUuid, testDocument); + expect(sanitizedDocument).toEqual(testDocument); + }); + + test('kubernetes system usernames are sanitized from a document when not system users', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + audit: { + user: { + username: 'user1', + groups: ['group1', 'group2', 'group3'], + }, + impersonated_user: { + username: 'impersonatedUser1', + groups: ['group4', 'group5', 'group6'], + }, + }, + }, + }; + + const sanitizedDocument = processK8sUsernames(clusterUuid, testDocument); + expect(sanitizedDocument).toEqual(testDocument); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts index f03621899c800b..f5d6bc41ee349b 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts @@ -8,8 +8,9 @@ import moment from 'moment'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { PackagePolicy } from '@kbn/fleet-plugin/common/types/models/package_policy'; -import { merge } from 'lodash'; +import { merge, set } from 'lodash'; import type { Logger } from '@kbn/core/server'; +import { sha256 } from 'js-sha256'; import { copyAllowlistedFields, filterList } from './filterlists'; import type { PolicyConfig, PolicyData } from '../../../common/endpoint/types'; import type { @@ -300,3 +301,47 @@ export const createTaskMetric = ( error_message: errorMessage, }; }; + +function obfuscateString(clusterId: string, toHash: string): string { + const valueToObfuscate = toHash + clusterId; + return sha256.create().update(valueToObfuscate).hex(); +} + +function isAllowlistK8sUsername(username: string) { + return ( + username === 'edit' || + username === 'view' || + username === 'admin' || + username === 'elastic-agent' || + username === 'cluster-admin' || + username.startsWith('system') + ); +} + +export const processK8sUsernames = (clusterId: string, event: TelemetryEvent): TelemetryEvent => { + // if there is no kubernetes key, return the event as is + if (event.kubernetes === undefined && event.kubernetes === null) { + return event; + } + + const username = event?.kubernetes?.audit?.user?.username; + const impersonatedUser = event?.kubernetes?.audit?.impersonated_user?.username; + + if (username !== undefined && username !== null && !isAllowlistK8sUsername(username)) { + set(event, 'kubernetes.audit.user.username', obfuscateString(clusterId, username)); + } + + if ( + impersonatedUser !== undefined && + impersonatedUser !== null && + !isAllowlistK8sUsername(impersonatedUser) + ) { + set( + event, + 'kubernetes.audit.impersonated_user.username', + obfuscateString(clusterId, impersonatedUser) + ); + } + + return event; +}; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts index a7fab953dad385..0fdc6cf32a69c0 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts @@ -11,7 +11,7 @@ import type { ITelemetryReceiver } from '../receiver'; import type { ESClusterInfo, ESLicense, TelemetryEvent } from '../types'; import type { TaskExecutionPeriod } from '../task'; import { TELEMETRY_CHANNEL_DETECTION_ALERTS, TASK_METRICS_CHANNEL } from '../constants'; -import { batchTelemetryRecords, tlog, createTaskMetric } from '../helpers'; +import { batchTelemetryRecords, createTaskMetric, processK8sUsernames, tlog } from '../helpers'; import { copyAllowlistedFields, filterList } from '../filterlists'; export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: number) { @@ -70,7 +70,12 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n copyAllowlistedFields(filterList.prebuiltRulesAlerts, event) ); - const enrichedAlerts = processedAlerts.map( + const sanitizedAlerts = processedAlerts.map( + (event: TelemetryEvent): TelemetryEvent => + processK8sUsernames(clusterInfo?.cluster_uuid, event) + ); + + const enrichedAlerts = sanitizedAlerts.map( (event: TelemetryEvent): TelemetryEvent => ({ ...event, licence_id: licenseInfo?.uid, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index ba61f6b85aaab4..df3b571714b298 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -64,6 +64,19 @@ export interface TelemetryEvent { id?: string; kind?: string; }; + kubernetes?: { + audit?: { + user?: { + username?: string; + groups?: string[]; + }; + impersonated_user?: { + username?: string; + groups?: string[]; + }; + pod?: SearchTypes; + }; + }; } // EP Policy Response From d5f12ac22fd505843990593ee5954e89202224e0 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 24 Apr 2023 16:20:20 -0600 Subject: [PATCH 65/65] [ML] Data Frame Analytics custom URLs: adds ability to set custom time range in urls (#155337) ## Summary Related meta issue: https://github.com/elastic/kibana/issues/150375 This PR adds a custom time range picker to the custom urls UI for Data Frame Analytics jobs. When not selected, the timerange will default to the global timerange - this is the same behavior as before. image image image ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../custom_time_range_picker.tsx | 156 ++++++++++++++++++ .../custom_urls/custom_url_editor/editor.tsx | 107 +++++++----- .../custom_urls/custom_url_editor/utils.ts | 28 +++- .../components/custom_urls/custom_urls.tsx | 4 + 4 files changed, 249 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx new file mode 100644 index 00000000000000..620aabd1c842bc --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useMemo, useState } from 'react'; +import moment, { type Moment } from 'moment'; +import { + EuiDatePicker, + EuiDatePickerRange, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiIconTip, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../contexts/kibana'; + +interface CustomUrlTimeRangePickerProps { + onCustomTimeRangeChange: (customTimeRange?: { start: Moment; end: Moment }) => void; + customTimeRange?: { start: Moment; end: Moment }; +} + +/* + * React component for the form for adding a custom time range. + */ +export const CustomTimeRangePicker: FC = ({ + onCustomTimeRangeChange, + customTimeRange, +}) => { + const [showCustomTimeRangeSelector, setShowCustomTimeRangeSelector] = useState(false); + const { + services: { + data: { + query: { + timefilter: { timefilter }, + }, + }, + }, + } = useMlKibana(); + + const onCustomTimeRangeSwitchChange = (checked: boolean) => { + if (checked === false) { + // Clear the custom time range so it isn't persisted + onCustomTimeRangeChange(undefined); + } + setShowCustomTimeRangeSelector(checked); + }; + + // If the custom time range is not set, default to the timefilter settings + const currentTimeRange = useMemo( + () => + customTimeRange ?? { + start: moment(timefilter.getAbsoluteTime().from), + end: moment(timefilter.getAbsoluteTime().to), + }, + [customTimeRange, timefilter] + ); + + const handleStartChange = (date: moment.Moment) => { + onCustomTimeRangeChange({ ...currentTimeRange, start: date }); + }; + const handleEndChange = (date: moment.Moment) => { + onCustomTimeRangeChange({ ...currentTimeRange, end: date }); + }; + + const { start, end } = currentTimeRange; + + return ( + <> + + + + + + + + } + > + + } + checked={showCustomTimeRangeSelector} + onChange={(e) => onCustomTimeRangeSwitchChange(e.target.checked)} + compressed + /> + + + + {showCustomTimeRangeSelector ? ( + <> + + + } + > + end} + startDateControl={ + + } + endDateControl={ + + } + /> + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx index 315c60fab6a6f2..523f59c32f2249 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { ChangeEvent, useMemo, useState, useRef, useEffect, FC } from 'react'; +import React, { ChangeEvent, useState, useRef, useEffect, FC } from 'react'; +import { type Moment } from 'moment'; import { EuiComboBox, @@ -29,10 +30,11 @@ import { DataView } from '@kbn/data-views-plugin/public'; import { CustomUrlSettings, isValidCustomUrlSettingsTimeRange } from './utils'; import { isValidLabel } from '../../../util/custom_url_utils'; import { type DataFrameAnalyticsConfig } from '../../../../../common/types/data_frame_analytics'; -import { Job, isAnomalyDetectionJob } from '../../../../../common/types/anomaly_detection_jobs'; +import { type Job } from '../../../../../common/types/anomaly_detection_jobs'; import { TIME_RANGE_TYPE, TimeRangeType, URL_TYPE } from './constants'; import { UrlConfig } from '../../../../../common/types/custom_urls'; +import { CustomTimeRangePicker } from './custom_time_range_picker'; import { useMlKibana } from '../../../contexts/kibana'; import { getDropDownOptions } from './get_dropdown_options'; @@ -66,6 +68,7 @@ interface CustomUrlEditorProps { dashboards: Array<{ id: string; title: string }>; dataViewListItems: DataViewListItem[]; showTimeRangeSelector?: boolean; + showCustomTimeRangeSelector: boolean; job: Job | DataFrameAnalyticsConfig; } @@ -78,10 +81,12 @@ export const CustomUrlEditor: FC = ({ savedCustomUrls, dashboards, dataViewListItems, + showTimeRangeSelector, + showCustomTimeRangeSelector, job, }) => { const [queryEntityFieldNames, setQueryEntityFieldNames] = useState([]); - const isAnomalyJob = useMemo(() => isAnomalyDetectionJob(job), [job]); + const [hasTimefield, setHasTimefield] = useState(false); const { services: { @@ -101,6 +106,9 @@ export const CustomUrlEditor: FC = ({ } catch (e) { dataViewToUse = undefined; } + if (dataViewToUse && dataViewToUse.timeFieldName) { + setHasTimefield(true); + } const dropDownOptions = await getDropDownOptions(isFirst.current, job, dataViewToUse); setQueryEntityFieldNames(dropDownOptions); @@ -132,6 +140,13 @@ export const CustomUrlEditor: FC = ({ }); }; + const onCustomTimeRangeChange = (timeRange?: { start: Moment; end: Moment }) => { + setEditCustomUrl({ + ...customUrl, + customTimeRange: timeRange, + }); + }; + const onDashboardChange = (e: ChangeEvent) => { const kibanaSettings = customUrl.kibanaSettings; setEditCustomUrl({ @@ -345,58 +360,66 @@ export const CustomUrlEditor: FC = ({ /> )} + {type === URL_TYPE.KIBANA_DASHBOARD || + (type === URL_TYPE.KIBANA_DISCOVER && showCustomTimeRangeSelector && hasTimefield) ? ( + + ) : null} - {(type === URL_TYPE.KIBANA_DASHBOARD || type === URL_TYPE.KIBANA_DISCOVER) && isAnomalyJob && ( - <> - - - - - } - className="url-time-range" - display="rowCompressed" - > - - - - {timeRange.type === TIME_RANGE_TYPE.INTERVAL && ( - + {(type === URL_TYPE.KIBANA_DASHBOARD || type === URL_TYPE.KIBANA_DISCOVER) && + showTimeRangeSelector && ( + <> + + + } className="url-time-range" - error={invalidIntervalError} - isInvalid={isInvalidTimeRange} display="rowCompressed" > - - )} - - - )} + {timeRange.type === TIME_RANGE_TYPE.INTERVAL && ( + + + } + className="url-time-range" + error={invalidIntervalError} + isInvalid={isInvalidTimeRange} + display="rowCompressed" + > + + + + )} + + + )} {type === URL_TYPE.OTHER && ( { // Get the complete list of attributes for the selected dashboard (query, filters). const { dashboardId, queryFieldNames } = settings.kibanaSettings ?? {}; @@ -253,11 +269,13 @@ async function buildDashboardUrlFromSettings(settings: CustomUrlSettings): Promi const dashboard = getDashboard(); + const { from, to } = getUrlRangeFromSettings(settings); + const location = await dashboard?.locator?.getLocation({ dashboardId, timeRange: { - from: '$earliest$', - to: '$latest$', + from, + to, mode: 'absolute', }, filters, @@ -299,10 +317,12 @@ function buildDiscoverUrlFromSettings(settings: CustomUrlSettings) { // Add time settings to the global state URL parameter with $earliest$ and // $latest$ tokens which get substituted for times around the time of the // anomaly on which the URL will be run against. + const { from, to } = getUrlRangeFromSettings(settings); + const _g = rison.encode({ time: { - from: '$earliest$', - to: '$latest$', + from, + to, mode: 'absolute', }, }); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx index 9d3db04fa40de4..4f9ad5245cf91f 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx @@ -42,6 +42,8 @@ import { import { openCustomUrlWindow } from '../../util/custom_url_utils'; import { UrlConfig } from '../../../../common/types/custom_urls'; import type { CustomUrlsWrapperProps } from './custom_urls_wrapper'; +import { isAnomalyDetectionJob } from '../../../../common/types/anomaly_detection_jobs'; +import { isDataFrameAnalyticsConfigs } from '../../../../common/types/data_frame_analytics'; const MAX_NUMBER_DASHBOARDS = 1000; @@ -206,6 +208,8 @@ class CustomUrlsUI extends Component { const editMode = this.props.editMode ?? 'inline'; const editor = (